RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.78k stars 1.29k forks source link

Enum is not generated with the correct integer values #3205

Open craigambrose32 opened 3 years ago

craigambrose32 commented 3 years ago

I'm using NSwag.CodeGeneration.CSharp v13.9.4 to generate a C# Http Client. Here is how I generate the client code:

public string GenerateClient(string swaggerSpec)
{
    var document = OpenApiDocument.FromJsonAsync(swaggerSpec).Result;
    var settings = new CSharpClientGeneratorSettings
    {
        ClassName = "MyHttpClient",
        UseBaseUrl = false,
        GenerateBaseUrlProperty = false,
        GenerateUpdateJsonSerializerSettingsMethod = true,
        InjectHttpClient = true,
        DisposeHttpClient = false,
        UseHttpClientCreationMethod = false,
        UseHttpRequestMessageCreationMethod = false,
        GenerateClientInterfaces = true,
        GenerateSyncMethods = true,
    };

    settings.CSharpGeneratorSettings.Namespace = "MyCompany.ApiClient;
    settings.CSharpGeneratorSettings.ArrayBaseType = "System.Collections.Generic.List";
    settings.CSharpGeneratorSettings.ArrayType = "System.Collections.Generic.IList";
    settings.CSharpGeneratorSettings.ArrayInstanceType = "System.Collections.Generic.List";
    settings.CSharpGeneratorSettings.DateTimeType = "System.DateTime";
    settings.CSharpGeneratorSettings.DateType = "System.DateTime";
    settings.ResponseArrayType = "System.Collections.Generic.IList";

    var generator = new CSharpClientGenerator(document, settings);  
    return generator.GenerateFile();
}

The originating C# enum and class looks like this (Note: the enum values are not sequential):

public enum StatusEnum
{
    New = 0,
    Updated = 10,
    Deleted = 20
}

public class Input
{
    [Required]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    public int Age { get; set; }

    public StatusEnum Status { get; set; }
}

With a simple controller that looks like this

[HttpPost]
[Route("submit")]
public Input Validate([FromBody] Input input)
{
    return input;
}

My server returns strings for the enum properties globally. So, I haven't added the annotation to either the enum definition or the references in the Input class. i.e.

    public static IMvcCoreBuilder AddJsonSettings(this IMvcCoreBuilder builder)
    {
        return builder.AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
            options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
            options.SerializerSettings.Formatting = Formatting.Indented;
        });
    }

Ultimately I end up with an OpenAPI spec that looks like this

{
  "openapi": "3.0.1",

<--- snip --->

  "components": {
    "schemas": {
      "StatusEnum": {
        "enum": [
          0,
          10,
          20
        ],
        "type": "string",
        "x-enumNames": [
          "New",
          "Updated",
          "Deleted"
        ]
      },
      "Input": {
        "required": [
          "email",
          "name"
        ],
        "type": "object",
        "properties": {
          "name": { 
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "age": {
            "type": "integer",
            "format": "int32"
          },
          "status": {
            "$ref": "#/components/schemas/StatusEnum"
          }
        },
        "additionalProperties": false
      }
    }
  }

<--- snip --->

}

Now when I generate the C# client code I end up with this for the enum and class (Note: the enum values are now sequential and don't match the originating class nor the values in the OpenAPI spec, although the EnumMember value does)

<--- snip --->
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.3.1.0 (Newtonsoft.Json v12.0.0.0)")]
public enum StatusEnum
{
    [System.Runtime.Serialization.EnumMember(Value = @"0")]
    New = 0,

    [System.Runtime.Serialization.EnumMember(Value = @"10")]
    Updated = 1,

    [System.Runtime.Serialization.EnumMember(Value = @"20")]
    Deleted = 2,    
}

[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.3.1.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class Input 
{
    [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)]
    [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
    public string Name { get; set; }

    [Newtonsoft.Json.JsonProperty("email", Required = Newtonsoft.Json.Required.Always)]
    [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
    public string Email { get; set; }

    [Newtonsoft.Json.JsonProperty("age", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
    public int Age { get; set; }

    [Newtonsoft.Json.JsonProperty("status", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
    [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
    public StatusEnum Status { get; set; }
}
<--- snip --->

If I change the OpenAPI spec to have the type of the enum be an integer like this:

{
  "openapi": "3.0.1",

<--- snip --->

  "components": {
    "schemas": {
      "StatusEnum": {
        "enum": [
          0,
          10,
          20
        ],
        "type": "integer",
        "x-enumNames": [
          "New",
          "Updated",
          "Deleted"
        ]
      },
      "Input": {
        "required": [
          "email",
          "name"
        ],
        "type": "object",
        "properties": {
          "name": { 
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "age": {
            "type": "integer",
            "format": "int32"
          },
          "status": {
            "$ref": "#/components/schemas/StatusEnum"
          }
        },
        "additionalProperties": false
      }
    }
  }

<--- snip --->

}

Then I get generated code like this that seems correct:

<--- snip --->
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.3.1.0 (Newtonsoft.Json v12.0.0.0)")]
public enum StatusEnum
{
    New = 0,    
    Updated = 10,
    Deleted = 20,
}

[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.3.1.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class Input 
{
    [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)]
    [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
    public string Name { get; set; }

    [Newtonsoft.Json.JsonProperty("email", Required = Newtonsoft.Json.Required.Always)]
    [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
    public string Email { get; set; }

    [Newtonsoft.Json.JsonProperty("age", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
    public int Age { get; set; }

    [Newtonsoft.Json.JsonProperty("now", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
    public System.DateTime Now { get; set; }

    [Newtonsoft.Json.JsonProperty("status", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
    public StatusEnum Status { get; set; }    
}
<--- snip --->

But I'd really like the OpenAPI spec to reflect that the enum values will typically be their string representations. Is this a bug with how the values of the enum are being generated? This is causing some problems on the caller side as I'd like the enum in the generated code to be the source of truth for what the underlying integer values are.

codingdna2 commented 3 years ago

Hello, I have the same issue. On the server side I use System.Text.Json and I generate the client using API Explorer. I see that enums with Flags are numbered starting from one, while non-Flags enum are numbered starting from zero effectively changing the original numbering. This happens when I enable the JsonStringEnumConverter on the server side. I guess the API Explorer just returns the string representation (my assumption) and NSwag doesn't know the original value.

RicoSuter commented 3 years ago

Flags enums are not really supported by OpenAPI and support is only done half-way in NSwag/NJsonSchema.

I recommend to use an enum array instead of flag enums.

If the serializer uses string enums in the JSON representation, then the numbers in the client should not matter as it always serializes via string. For flag enums and string I do not know how the serializer represents multiple values in a single string - but it is probably not compliant/not possible to describe with OpenAPI.

codingdna2 commented 3 years ago

Hi Rico, thanks for your answer. In my project, System.Text.Json serialize Flags separating values with a comma. Not sure if it's compliant but it works.

The original value matter if it's used against a database or, as in my case, to communicate with a device. Not the best practice for sure but it's like this in real world.

Finally I was inspecting the EnumMember attribute and there's no property to carry its original value... I ended up modifying my Flags enum to start from one (removing the None value). I guess it's just something to remember ;)