5

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);
        }
    }
}
Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • Yep, ugly custom generic type gets ugly `ISchemaFilter` ... do you need that generic type? if it's ultimately mapped to a string why can it not just be a string ... or maybe the SwaggerResponse is a cleaner alternative `[SwaggerResponse(201, "The product was created", typeof(Product))]` – Helder Sepulveda Mar 26 '20 at 14:01
  • I do need it to be generic. It carries type information that I use in filters to validate the value. – Thomas Levesque Mar 26 '20 at 16:30
  • You want it to be generic, but it does not need to be... make it a string then parse/validate what you need... and for the record the `I...Filter` solution is used in the code in "ugly" ways too https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b64b8fd6fbc7959849445be676f5e3d4a8e947bf/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs – Helder Sepulveda Mar 26 '20 at 16:34

1 Answers1

2

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.

sasquatch
  • 29
  • 3