Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to expose a custom generic type as a string in Swagger schema using Swashbuckle.AspNetCore

I have a custom generic type that looks roughly like this:

public struct Foo<T>
{
    public int Value { get; }
    public string Signature { get; }
    public Type Type { get; }
}

This type is is used in request and response bodies and in controller action parameters. Everything is configured so that it's serialized as a string, and it works fine with model binding and JSON serialization. The type has a TypeConverter associated with it, which takes care of converting it to and from a string.

However, the Swagger schema still represents it as an object with 3 properties. The Type property is also expanded, which pulls all the System.Reflection types exposed directly or indirectly by Type.

How can I avoid this and expose my type as a string?


First solution attempted: using MapType

I tried to use MapType; it works fine if I specify the generic type argument, but doesn't work with the open generic type:

c.MapType(typeof(Foo<Something>), () => new OpenApiSchema { Type = "string" }); // Works
c.MapType(typeof(Foo<>), () => new OpenApiSchema { Type = "string" }); // Doesn't work

How can I apply the mapping to Foo<T>, for any T?


Current workaround

So far the only workaround I have is pretty ugly :

class SchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (context.Type is Type type &&
            type.IsGenericType &&
            !type.IsGenericTypeDefinition &&
            type.GetGenericTypeDefinition() == typeof(Foo<>))
        {
            schema.Type = "string";
            schema.Properties.Clear();
        }
        else if (context.Type?.FullName.StartsWith("System.", StringComparison.Ordinal) is true
            && context.SchemaRepository.TryGetIdFor(context.Type, out var schemaId))
        {
            DocFilter.SchemaIdsToRemove.Add(schemaId);
        }
    }
}

class DocFilter : IDocumentFilter
{
    public static readonly HashSet<string> SchemaIdsToRemove = new HashSet<string>();

    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        foreach (var schemaId in SchemaIdsToRemove)
        {
            swaggerDoc.Components.Schemas.Remove(schemaId);
        }
    }
}
like image 754
Thomas Levesque Avatar asked Nov 07 '22 08:11

Thomas Levesque


1 Answers

i am not sure what you are trying to do. Because the best thing if i understood your scenario correctly would be to expose one schema definition for each underlying type of the Foo generic type, code then would be something like:

public class FooSchemaFilter : ISchemaFilter
    {
        public void Apply(OpenApiSchema schema, SchemaFilterContext context)
        {
            if (context.Type.IsGenericType && context.Type.GetGenericTypeDefinition() == typeof(Foo<>))
            {
                var argumentType = context.Type.GetGenericArguments().First();
                var argumentSchema = context.SchemaGenerator.GenerateSchema(argumentType, context.SchemaRepository);
                var baseSchemaName = $"{argumentType.Name}Foo";
                var baseSchema = new OpenApiSchema()
                {
                    Required = new SortedSet<string>() { "type" },
                    Type = "object",
                    Properties = new Dictionary<string, OpenApiSchema> {
                        { "type", argumentSchema }
                };
                context.SchemaRepository.AddDefinition(baseSchemaName, baseSchema);
                schema.Type = "string";
                schema.Reference = new OpenApiReference { Id = $"{baseSchemaName}", Type = ReferenceType.Schema };
            }
        }
    }

if you need the other properties as well then include them in the base schema. This will create a new schema for every type but it should deserialize into your generic type.

like image 52
sasquatch Avatar answered Nov 14 '22 08:11

sasquatch