Skip to content

OpenApi generates the same schema for generic types with a JsonConverterFactory. #59172

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
1 task done
desjoerd opened this issue Nov 26, 2024 · 7 comments
Open
1 task done
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi
Milestone

Comments

@desjoerd
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When generating openapi containing Generic types, with a JsonConverterFactory, the same schema is generated for different types.

Given the following minimal example:

using System.Text.Json;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

app.UseHttpsRedirection();
app.MapOpenApi();

app.MapGet("/", () => new ExampleModel());

app.Run();

public class GenericConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert) => typeToConvert.GetGenericTypeDefinition() == typeof(GenericValue<>);

    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => (JsonConverter?)Activator.CreateInstance(typeof(GenericConverter<>).MakeGenericType(typeToConvert.GetGenericArguments()[0]));
}

public class GenericConverter<T> : JsonConverter<GenericValue<T>>
{
    public override GenericValue<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException();

    public override void Write(Utf8JsonWriter writer, GenericValue<T> value, JsonSerializerOptions options) => throw new NotImplementedException();
}

[JsonConverter(typeof(GenericConverterFactory))]
public readonly struct GenericValue<TId>
{
    public TId Id { get; init; }
}

public class ExampleModel
{
    public GenericValue<Guid> GuidValue { get; set; }
    public GenericValue<string> StringValue { get; set; }
}

I am getting the following openapi:

{
  "openapi": "3.0.1",
  "info": {
    "title": "MinimalApi | v1",
    "version": "1.0.0"
  },
  "paths": {
    "/": {
      "get": {
        "tags": [
          "MinimalApi"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExampleModel"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ExampleModel": {
        "type": "object",
        "properties": {
          "guidValue": {
            "$ref": "#/components/schemas/GenericValueOfstring"
          },
          "stringValue": {
            "$ref": "#/components/schemas/GenericValueOfstring"
          }
        }
      },
      "GenericValueOfstring": { }
    }
  },
  "tags": [
    {
      "name": "MinimalApi"
    }
  ]
}

There is only one schema generated for the GenericValue, namely GenericValueOfstring (which is the last defined type). I was expecting GenericValueOfstring and GenericValueOfGuid.

Expected Behavior

No response

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

9.0.100

Anything else?

No response

@ghost ghost added the old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Nov 26, 2024
@martincostello martincostello added feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Nov 26, 2024
@desjoerd
Copy link
Author

After some more investigation, I think this is caused by dotnet/runtime#110241

@desjoerd
Copy link
Author

After even more investigation, the cause of this is probably fixed in dotnet/runtime#109868. I am fine if someone wants to close this, otherwise I will wait for the next servicing release to test this.

@captainsafia
Copy link
Member

@desjoerd Thanks for filing this issue and taking the time to investigate it yourself.

I'll keep this open so that we can follow up once the next servicing release comes out.

The reference PR does seem to solve the issue but it would be good to verify that there isn't a confounding issue in Microsoft.AspNetCore.OpenApi that is also causing the problem.

In the meantime, sticking this in the backlog until we verify.

@captainsafia captainsafia added this to the Backlog milestone Dec 18, 2024
@desjoerd
Copy link
Author

@captainsafia I just checked with .NET Runtime 9.0.1 and I now get the correct output, a GenericValueOfGuid AND GenericValueOfstring. So in my opinion this issue can be closed 👍

{
  "openapi": "3.0.1",
  "info": {
    "title": "MinimalApi | v1",
    "version": "1.0.0"
  },
  "paths": {
    "/": {
      "get": {
        "tags": [
          "MinimalApi"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExampleModel"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ExampleModel": {
        "type": "object",
        "properties": {
          "guidValue": {
            "$ref": "#/components/schemas/GenericValueOfGuid"
          },
          "stringValue": {
            "$ref": "#/components/schemas/GenericValueOfstring"
          }
        }
      },
      "GenericValueOfGuid": { },
      "GenericValueOfstring": { }
    }
  },
  "tags": [
    {
      "name": "MinimalApi"
    }
  ]
}

@Cyberzim
Copy link

There are still issues when using JsonConverterFactory. I am using dotnet v9.0.300
The schema is empty for the involved generic type. In my case, I have the following struct defined:

public readonly struct OptionalValue<T>
{
  public T Value { get; init; }
  public bool IsSet { get; init; }

  public OptionalValue()
  {
    Value = default!;
    IsSet = false;
  }

  public OptionalValue(T value)
  {
    Value = value;
    IsSet = true;
  }
}

And here is an example of a model using this type:

public record AccountPatchRequest
{
  public OptionalValue<string> FirstName { get; init; }
  public OptionalValue<string> LastName { get; init; }
  public OptionalValue<Language?> PreferredLanguage { get; init; }
}

Without the JsonConverterFactory, I get the correct schema:

Image

As soon as I add the JsonConverterFactory to the JsonOptions in my minimal API, the generated schema for the OptionalValue<T> types are empty:

Image

@desjoerd
Copy link
Author

desjoerd commented May 31, 2025

@Cyberzim you will have to do some unwrapping. I've done that for you with .NET 10 because it has the GetOrCreateSchema method: https://github.com/desjoerd/OptionalValues/blob/feature/net10/examples/OptionalValues.Examples.OpenApi/Program.cs

It's the same for NSwag or Swashbuckle, which I've implemented and published. For .NET 10 I can publish a preview as well if you want.

Edit:
The empty schemas are expected. As aspnetcore does not know the format of your json.

@Cyberzim
Copy link

Cyberzim commented May 31, 2025

Thank you for sharing this. Unfortunately switching to .NET 10 is not an option for now until it is released. For now, the workaround for me is to just exclude the JsonConverterFactory when generating the OpenAPI specs. I should also note that I have a IOpenApiSchemaTransformer for transforming OptionalValue<T> into T.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi
Projects
None yet
Development

No branches or pull requests

4 participants