RicoSuter / NJsonSchema

JSON Schema reader, generator and validator for .NET
http://NJsonSchema.org
MIT License
1.4k stars 534 forks source link

NewtonSoft.Json.Required.DisallowNull set on non-required field #1631

Open bstordrup opened 1 year ago

bstordrup commented 1 year ago

I have a schema that contains a MasterData like this:

            "MasterDataDto": {
                "required": ["companyId", "deleted", "masterDataTypeName", "message", "publicId", "synchronizedVersion", "version"],
                "type": "object",
                "properties": {
                    "masterDataTypeName": {
                        "type": "string"
                    },
                    "publicId": {
                        "type": "string"
                    },
                    "companyId": {
                        "type": "integer",
                        "format": "int64"
                    },
                    "version": {
                        "type": "integer",
                        "format": "int64"
                    },
                    "synchronizedVersion": {
                        "type": "integer",
                        "format": "int64"
                    },
                    "correlationId": {
                        "type": "string"
                    },
                    "deleted": {
                        "type": "boolean"
                    },
                    "message": {
                        "type": "string"
                    },
                    "createdDate": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "updatedDate": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "lastOperationId": {
                        "type": "string"
                    }
                }
            },

Only the properties mentioned in the "required" property are required.

But in the Dto class generated, all properties are generated with the NewtonSoft.Json.Required.DisallowNull value for the JsonProperty.Required property in the JsonProperty attribute:

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial class MasterDataDto
    {
        [Newtonsoft.Json.JsonProperty("masterDataTypeName", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
        public string MasterDataTypeName { get; set; }

        [Newtonsoft.Json.JsonProperty("publicId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
        public string PublicId { get; set; }

        [Newtonsoft.Json.JsonProperty("companyId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public long CompanyId { get; set; }

        [Newtonsoft.Json.JsonProperty("version", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public long Version { get; set; }

        [Newtonsoft.Json.JsonProperty("synchronizedVersion", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public long SynchronizedVersion { get; set; }

        [Newtonsoft.Json.JsonProperty("correlationId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string CorrelationId { get; set; }

        [Newtonsoft.Json.JsonProperty("deleted", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public bool Deleted { get; set; }

        [Newtonsoft.Json.JsonProperty("message", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
        public string Message { get; set; }

        [Newtonsoft.Json.JsonProperty("createdDate", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public System.DateTimeOffset? CreatedDate { get; set; }

        [Newtonsoft.Json.JsonProperty("updatedDate", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public System.DateTimeOffset? UpdatedDate { get; set; }

        [Newtonsoft.Json.JsonProperty("lastOperationId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string LastOperationId { get; set; }

        private System.Collections.Generic.IDictionary<string, object> _additionalProperties;

        [Newtonsoft.Json.JsonExtensionData]
        public System.Collections.Generic.IDictionary<string, object> AdditionalProperties
        {
            get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary<string, object>()); }
            set { _additionalProperties = value; }
        }

    }

What do I need to set to get the generator to respect the "required" property list? My code generators settings are:

  "codeGenerators": {
    "openApiToCSharpClient": {
      "clientBaseClass": null,
      "configurationClass": null,
      "generateClientClasses": true,
      "generateClientInterfaces": true,
      "clientBaseInterface": null,
      "injectHttpClient": true,
      "disposeHttpClient": true,
      "protectedMethods": [],
      "generateExceptionClasses": true,
      "exceptionClass": "MdmRestApiException",
      "wrapDtoExceptions": true,
      "useHttpClientCreationMethod": false,
      "httpClientType": "System.Net.Http.HttpClient",
      "useHttpRequestMessageCreationMethod": false,
      "useBaseUrl": true,
      "generateBaseUrlProperty": true,
      "generateSyncMethods": false,
      "generatePrepareRequestAndProcessResponseAsAsyncMethods": false,
      "exposeJsonSerializerSettings": true,
      "clientClassAccessModifier": "public",
      "typeAccessModifier": "public",
      "generateContractsOutput": true,
      "contractsNamespace": "Visma.Business.MdmRestClient",
      "contractsOutputFilePath": "IMdmRestApi.cs",
      "parameterDateTimeFormat": "s",
      "parameterDateFormat": "yyyy-MM-dd",
      "generateUpdateJsonSerializerSettingsMethod": true,
      "useRequestAndResponseSerializationSettings": false,
      "serializeTypeInformation": false,
      "queryNullValue": "",
      "className": "{controller}MdmRestApiClient",
      "operationGenerationMode": "SingleClientFromOperationId",
      "additionalNamespaceUsages": [],
      "additionalContractNamespaceUsages": [],
      "generateOptionalParameters": false,
      "generateJsonMethods": false,
      "enforceFlagEnums": false,
      "parameterArrayType": "System.Collections.Generic.IEnumerable",
      "parameterDictionaryType": "System.Collections.Generic.IDictionary",
      "responseArrayType": "System.Collections.Generic.ICollection",
      "responseDictionaryType": "System.Collections.Generic.IDictionary",
      "wrapResponses": false,
      "wrapResponseMethods": [],
      "generateResponseClasses": true,
      "responseClass": "SwaggerResponse",
      "namespace": "Visma.Business.MdmRestClient",
      "requiredPropertiesMustBeDefined": false,
      "dateType": "System.DateTimeOffset",
      "jsonConverters": null,
      "anyType": "object",
      "dateTimeType": "System.DateTimeOffset",
      "timeType": "System.TimeSpan",
      "timeSpanType": "System.TimeSpan",
      "arrayType": "System.Collections.Generic.ICollection",
      "arrayInstanceType": "System.Collections.ObjectModel.Collection",
      "dictionaryType": "System.Collections.Generic.IDictionary",
      "dictionaryInstanceType": "System.Collections.Generic.Dictionary",
      "arrayBaseType": "System.Collections.ObjectModel.Collection",
      "dictionaryBaseType": "System.Collections.Generic.Dictionary",
      "classStyle": "Poco",
      "jsonLibrary": "NewtonsoftJson",
      "generateDefaultValues": true,
      "generateDataAnnotations": true,
      "excludedTypeNames": [],
      "excludedParameterNames": [],
      "handleReferences": false,
      "generateImmutableArrayProperties": false,
      "generateImmutableDictionaryProperties": false,
      "jsonSerializerSettingsTransformationMethod": null,
      "inlineNamedArrays": false,
      "inlineNamedDictionaries": false,
      "inlineNamedTuples": true,
      "inlineNamedAny": false,
      "generateDtoTypes": true,
      "generateOptionalPropertiesAsNullable": true,
      "generateNullableReferenceTypes": false,
      "templateDirectory": null,
      "typeNameGeneratorType": null,
      "propertyNameGeneratorType": null,
      "enumNameGeneratorType": null,
      "serviceHost": null,
      "serviceSchemes": null,
      "output": "MdmRestApi.cs",
      "newLineBehavior": "Auto"
    }
  }

It seems like the generator does not handle a string based property as a property that can contain null.

trejjam commented 1 year ago

For generating a nullable contract you need to set "generateNullableReferenceTypes": true, if I am not wrong

bstordrup commented 1 year ago

Doing so will mark the property as string? (and also other properties as int?).

But even without this, the string type can be null and is in cases also containing null in the response from the API in question. And that fails because the generated property is decorated with Required = NewtonSoft.Json.Required.DisallowNull

Might be a corner case that a non-required string field in the response should not be marked with DisallowNull, but with Default instead.

RicoSuter commented 1 year ago

The generated code is correct as none of your properties allow null, you'd need to set the type to ["string", "null"] to make it nullable

bstordrup commented 1 year ago

So, the definition for corelationId should be

                    "correlationId": {
                        "type": ["string", "null"]
                    },

in the Json behind the Swagger definition?

But that means that if I do not have control over the API, I'm not able to generate a correct class definition. Because the string can have the value "null" even though the API Json definition does not have the "null" part of the definition. As it it not referenced in the "required" list at the top of the definition.

trejjam commented 1 year ago

Since you do not have necessary information in source document you can't do much in NSwag.

You can set Nullability or you can specify Required properties. One generates nullable types directly, the other works with generateNullableReferenceTypes.

When the source document is not correct you can write your preprocessor which by some rules fix the source document.

RicoSuter commented 1 year ago

Ah this is not JSON Schema.. in OpenAPI you can set “nullable” to true on the property