Skip to content

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

@desjoerd

Description

@desjoerd
Contributor

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

Activity

ghost added
old-area-web-frameworks-do-not-use*DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
on Nov 26, 2024
added
area-minimalIncludes 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
on Nov 26, 2024
desjoerd

desjoerd commented on Nov 28, 2024

@desjoerd
ContributorAuthor

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

desjoerd

desjoerd commented on Nov 28, 2024

@desjoerd
ContributorAuthor

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

captainsafia commented on Dec 18, 2024

@captainsafia
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.

added this to the Backlog milestone on Dec 18, 2024
desjoerd

desjoerd commented on Jan 15, 2025

@desjoerd
ContributorAuthor

@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

Cyberzim commented on May 31, 2025

@Cyberzim

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

desjoerd commented on May 31, 2025

@desjoerd
ContributorAuthor

@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

Cyberzim commented on May 31, 2025

@Cyberzim

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.

desjoerd

desjoerd commented on Jul 31, 2025

@desjoerd
ContributorAuthor

I see that this is still open. Since 9.0.1 it gives the correct behavior, for generic types without a converter it generates complete schemas and for generic types with a converter empty ones (which are seperate instances so they can be transformed). So I am closing 🙂.

1 remaining item

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-openapi

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @martincostello@captainsafia@desjoerd@Cyberzim

        Issue actions

          OpenApi generates the same schema for generic types with a JsonConverterFactory. · Issue #59172 · dotnet/aspnetcore