Nullable Strings not being marked correctly
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 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?
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?
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 --disposableThis 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
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.