refitter icon indicating copy to clipboard operation
refitter copied to clipboard

Nullable Strings not being marked correctly

Open JamesFieldist opened this issue 1 year ago • 4 comments

Describe the bug Nullable strings in the openapi.json marked correctly are not being made string? in contracts.

Consider the following openapi.json snippet:

      "AddressDto": {
        "required": [
          "street",
          "city",
          "country",
          "postalCode",
          "isDefault",
          "isVerified",
          "isHome",
          "isWork",
          "id"
        ],
        "type": "object",
        "properties": {
          "street": {
            "type": "string"
          },
          "street2": {
            "type": "string",
            "nullable": true
          },
          "street3": {
            "type": "string",
            "nullable": true
          },
          "careOf": {
            "type": "string",
            "nullable": true
          },
          "city": {
            "type": "string"
          },
          "stateProvince": {
            "type": "string",
            "nullable": true
          },
          "country": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "latitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "longitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "owningContactId": {
            "type": "string",
            "nullable": true
          },
          "isDefault": {
            "type": "boolean"
          },
          "isVerified": {
            "type": "boolean"
          },
          "isHome": {
            "type": "boolean"
          },
          "isWork": {
            "type": "boolean"
          },
          "id": {
            "type": "string",
            "description": "Id. Null if new.",
            "format": "uuid",
            "nullable": true
          }
        }
      },

This produces this C#:

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial record AddressDto
    {
        [System.Text.Json.Serialization.JsonConstructor]

        public AddressDto(string @careOf, string @city, string @country, System.Guid? @id, bool @isDefault, bool @isHome, bool @isVerified, bool @isWork, double? @latitude, double? @longitude, string @owningContactId, string @postalCode, string @stateProvince, string @street, string @street2, string @street3)

        {

            this.Street = @street;

            this.Street2 = @street2;

            this.Street3 = @street3;

            this.CareOf = @careOf;

            this.City = @city;

            this.StateProvince = @stateProvince;

            this.Country = @country;

            this.PostalCode = @postalCode;

            this.Latitude = @latitude;

            this.Longitude = @longitude;

            this.OwningContactId = @owningContactId;

            this.IsDefault = @isDefault;

            this.IsVerified = @isVerified;

            this.IsHome = @isHome;

            this.IsWork = @isWork;

            this.Id = @id;

        }
        [System.Text.Json.Serialization.JsonPropertyName("street")]
        [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
        public string Street { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("street2")]
        public string Street2 { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("street3")]
        public string Street3 { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("careOf")]
        public string CareOf { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("city")]
        [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
        public string City { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("stateProvince")]
        public string StateProvince { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("country")]
        [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
        public string Country { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("postalCode")]
        [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
        public string PostalCode { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("latitude")]
        public double? Latitude { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("longitude")]
        public double? Longitude { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("owningContactId")]
        public string OwningContactId { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("isDefault")]
        public bool IsDefault { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("isVerified")]
        public bool IsVerified { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("isHome")]
        public bool IsHome { get; init; }

        [System.Text.Json.Serialization.JsonPropertyName("isWork")]
        public bool IsWork { get; init; }

        /// <summary>
        /// Id. Null if new.
        /// </summary>

        [System.Text.Json.Serialization.JsonPropertyName("id")]
        public System.Guid? Id { get; init; }

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

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

    }

Note that Street2 is not marked as string? etc. Only the doubles are.

And that's using the following:

dotnet tool run refitter https://localhost:15159/help/v1/openapi.json --output ./ --namespace TestClient.Interfaces --contracts-namespace TestClient.Contracts --trim-unused-schema --immutable-records --cancellation-tokens --multiple-files --no-deprecated-operations --optional-nullable-parameters --nullable-reference-types --disposable

This should have produced nullable string values.

Support Key: [my-support-key]

lxoej4f

Additional context This appears to only be a problem with strings. nswag does this correctly from the definition provided.

JamesFieldist avatar Jan 10 '25 16:01 JamesFieldist

@JamesFieldist Thanks for taking the time to report this

Refitter uses NSwag for parsing the OpenAPI document and for generating the contracts. If NSwag can do this out-of-the-box then I need to align how Refitter uses NSwag with that behavior. Let me see what I can do. Refitter caters to quite a number of users which have introduced multiple options for generating contracts

Do you use NSwag from the desktop app or CLI? Do you mind sharing the options you use with NSwag?

christianhelle avatar Jan 11 '25 08:01 christianhelle

nswag openapi2csclient /input:your-schema.json /output:Client.cs /nullableReferenceTypes:true /generateNullableProperties:true

Although there seems to be some claims that you have to use nullable:true in the openapi.json documentation file, but it looks like that was fixed?

JamesFieldist avatar Jan 14 '25 17:01 JamesFieldist

And that's using the following:

dotnet tool run refitter https://localhost:15159/help/v1/openapi.json --output ./ --namespace TestClient.Interfaces --contracts-namespace TestClient.Contracts --trim-unused-schema --immutable-records --cancellation-tokens --multiple-files --no-deprecated-operations --optional-nullable-parameters --nullable-reference-types --disposable

This should have produced nullable string values.

@JamesFieldist Sorry, I just overlooked that the --optional-nullable-parameters --nullable-reference-types options don't exist as CLI arguments in Refitter. These are exposed through the settings file. Try using a .refitter settings file

Try creating a .refitter file with the following contents

{
  "openApiPath": "https://localhost:15159/help/v1/openapi.json",
  "namespace": "TestClient.Interfaces",
  "contractsNamespace": "TestClient.Contracts",
  "trimUnusedSchema": true,
  "immutableRecords": true,
  "useCancellationTokens": true,
  "generateMultipleFiles": true,
  "generateDeprecatedOperations": false,  
  "codeGeneratorSettings": {
    "generateOptionalPropertiesAsNullable": true,
    "generateNullableReferenceTypes": true
  }
}

then run Refitter with the following arguments

refitter --settings-file .refitter

The .refitter file format is described in https://refitter.github.io/articles/refitter-file-format.html

christianhelle avatar Jan 15 '25 08:01 christianhelle

I'm also struggling with this a bit. When I follow @christianhelle's last example it creates constructors for all of my data contacts, and I'd prefer them to be public required MyType MyFieldName { get; init; } instead.

Is there a combination of settings I can use to achieve this? I'm looking into the NSwag documentation, and while I'm new to this library I think it has something to do with generating nominal records rather than positional records.

sb-chericks avatar Jul 31 '25 21:07 sb-chericks