Skip to content

Typescript Client generates with a compile error when used with JsonStringEnumConverter and returning a dictionary keyed by an Enum #5141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
mukund0299 opened this issue Apr 13, 2025 · 0 comments

Comments

@mukund0299
Copy link

Describe the bug

When generating a typescript client with class type style, if the backend returns a response that contains a dictionary with an enum and it is using the JsonStringEnumConverter, the generated ts client has a compile error in the toJson method of the containing object:

Image

Version of NSwag toolchain, computer and .NET runtime used

Backend: NSwag.AspNetCore 14.3.0, .NET 8.0
NSwagStudio: 14.3.0.0

To Reproduce

Server:

using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument();
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
    options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

var app = builder.Build();

app.MapGet("/test", () =>
{
    Dictionary<RandomEnum, Weather> dictionary = [];
    for (var i = 0; i < 5; i++)
    {
        dictionary.Add((RandomEnum) i, new Weather(Random.Shared.Next(-20, 55), "Random Text"));
    }
    return new WeatherForecast
    {
        Forecast = dictionary
    };
});

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi();
}

app.Run();


public enum RandomEnum
{
    Value1,
    Value2,
    Value3,
    Value4,
    Value5,
}

public class WeatherForecast
{
    public Dictionary<RandomEnum, Weather> Forecast { get; set; } = [];
}
public record Weather(int Temperature, string Summary);

Generated Open API spec:

{
  "x-generator": "NSwag v14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))",
  "openapi": "3.0.0",
  "info": {
    "title": "My Title",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://localhost:5164"
    }
  ],
  "paths": {
    "/test": {
      "get": {
        "operationId": "GetTest",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WeatherForecast"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "WeatherForecast": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "forecast": {
            "type": "object",
            "x-dictionaryKey": {
              "$ref": "#/components/schemas/RandomEnum"
            },
            "additionalProperties": {
              "$ref": "#/components/schemas/Weather"
            }
          }
        }
      },
      "RandomEnum": {
        "type": "string",
        "description": "",
        "x-enumNames": [
          "Value1",
          "Value2",
          "Value3",
          "Value4",
          "Value5"
        ],
        "enum": [
          "Value1",
          "Value2",
          "Value3",
          "Value4",
          "Value5"
        ]
      },
      "Weather": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "temperature": {
            "type": "integer",
            "format": "int32"
          },
          "summary": {
            "type": "string"
          }
        }
      }
    }
  }
}

Typescript client settings:

{
  "runtime": "Net80",
  "defaultVariables": null,
  "documentGenerator": {
    "fromDocument": {
      "url": "http://localhost:5164/swagger/v1/swagger.json",
      "output": null,
      "newLineBehavior": "Auto"
    }
  },
  "codeGenerators": {
    "openApiToTypeScriptClient": {
      "className": "{controller}Client",
      "moduleName": "",
      "namespace": "",
      "typeScriptVersion": 4.3,
      "template": "Axios",
      "promiseType": "Promise",
      "httpClass": "HttpClient",
      "withCredentials": false,
      "useSingletonProvider": false,
      "injectionTokenType": "OpaqueToken",
      "rxJsVersion": 6.0,
      "dateTimeType": "Date",
      "nullValue": "Undefined",
      "generateClientClasses": true,
      "generateClientInterfaces": false,
      "generateOptionalParameters": false,
      "exportTypes": true,
      "wrapDtoExceptions": false,
      "exceptionClass": "ApiException",
      "clientBaseClass": null,
      "wrapResponses": false,
      "wrapResponseMethods": [],
      "generateResponseClasses": true,
      "responseClass": "SwaggerResponse",
      "protectedMethods": [],
      "configurationClass": null,
      "useTransformOptionsMethod": false,
      "useTransformResultMethod": false,
      "generateDtoTypes": true,
      "operationGenerationMode": "MultipleClientsFromOperationId",
      "markOptionalProperties": false,
      "generateCloneMethod": false,
      "typeStyle": "Class",
      "enumStyle": "Enum",
      "useLeafType": false,
      "classTypes": [],
      "extendedClasses": [],
      "extensionCode": null,
      "generateDefaultValues": true,
      "excludedTypeNames": [],
      "excludedParameterNames": [],
      "handleReferences": false,
      "generateTypeCheckFunctions": false,
      "generateConstructorInterface": true,
      "convertConstructorInterfaceData": false,
      "importRequiredTypes": true,
      "useGetBaseUrlMethod": false,
      "baseUrlTokenName": "API_BASE_URL",
      "queryNullValue": "",
      "useAbortSignal": false,
      "inlineNamedDictionaries": false,
      "inlineNamedAny": false,
      "includeHttpContext": false,
      "templateDirectory": null,
      "serviceHost": null,
      "serviceSchemes": null,
      "output": null,
      "newLineBehavior": "Auto"
    }
  }
}

Snippet with error in the generated client:

export class WeatherForecast implements IWeatherForecast {
    forecast!: { [key in keyof typeof RandomEnum]?: Weather; };

    constructor(data?: IWeatherForecast) {
        if (data) {
            for (var property in data) {
                if (data.hasOwnProperty(property))
                    (<any>this)[property] = (<any>data)[property];
            }
        }
    }

    init(_data?: any) {
        if (_data) {
            if (_data["forecast"]) {
                this.forecast = {} as any;
                for (let key in _data["forecast"]) {
                    if (_data["forecast"].hasOwnProperty(key))
                        (<any>this.forecast)![key] = _data["forecast"][key] ? Weather.fromJS(_data["forecast"][key]) : new Weather();
                }
            }
        }
    }

    static fromJS(data: any): WeatherForecast {
        data = typeof data === 'object' ? data : {};
        let result = new WeatherForecast();
        result.init(data);
        return result;
    }

    toJSON(data?: any) {
        data = typeof data === 'object' ? data : {};
        if (this.forecast) {
            data["forecast"] = {};
            for (let key in this.forecast) {
                if (this.forecast.hasOwnProperty(key))
                    (<any>data["forecast"])[key] = this.forecast[key] ? this.forecast[key].toJSON() : <any>undefined; // error occurs here
            }
        }
        return data;
    }
}

Expected behavior

The client should generate without errors, presumably by casting the string key into an enum:
(<any>data["forecast"])[key] = this.forecast[key as RandomEnum] ? this.forecast[key as RandomEnum]?.toJSON() : <any>undefined;

Additional context

This doesn't happen if using interfaces to generate the client, or if the json string enum converter is not used.

It also doesn't happen if the value type of the dictionary is object, because it casts the entire value into , so it skips the type validation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant