Skip to content
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

[Feature request]: Attributes to delegate custom OpenApiSchemas for specific types #2990

Open
Barbiero opened this issue Jul 19, 2024 · 1 comment
Labels
feature suggestion help-wanted A change up for grabs for contributions from the community

Comments

@Barbiero
Copy link

Barbiero commented Jul 19, 2024

Is your feature request related to a specific problem? Or an existing feature?

Consider the following scenario, where I've defined a Value Object to encapsulate a custom-tailored Id

JsonConverter(typeof(ProductIdConverter))]
public readonly record struct ProductId
{
    public string Value { get; }
    public ProductId(string Value)
    {
        if (!ProductIdValidator.Validate(Value)) throw new InvalidProductIdException(Value);
        this.Value = Value;
    }
}

The idea of this kind of structure is to have a type that has its own validation, and can't be assigned wrongly to another similar type by mistake (imagine a situation where you have a "string productId" and a "string skuId" and they might be mismatched).

System.Text.Json is very nice here, because it allows me to create my own converter that parses the value and transforms it if necessary

public class ProductIdConverter : JsonConverter<ProductId>
{
    public override ProductId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var str = reader.GetString()!;
        // update the product id to a new version. idk, this is just an example.
        if (str.StartsWith("v1")) str = ProductIdLegacyUpdater.Update(str);
        return new ProductId(str);
    }

    public override void Write(Utf8JsonWriter writer, ProductId value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.Value);
    }
}

This allows me to make my boundaries open to string values without the API user having to understand this abstraction, but the problem is that Swagger will recognize this as an object by default. But that's fine, I can configure that on startup

builder.Services.AddSwaggerGen(options => 
{
    options.SchemaGeneratorOptions.CustomTypeMapping.Add(typeof(ProductId), 
        () => new OpenApiSchema
        {
            Type = "string",
            Example = new OpenApiString("v1abcdef")
        }
    );
});

however, that will require referencing the ProductId part on startup and it'll quickly get verbose. Of course, there are ways to organize this so it's in a separate package and all that, but that's not really what I want

Describe the solution you'd like

I wish I could add an attribute to my class/record/struct/enum that tells swagger what is the expected OpenApiSchema for that this type should be represented by, in a similar way that JsonConverter does it.

Think of it like this: Right now, we can use .AddJsonOptions to customize our JSON converters with our service builders, or we can use JsonConverterAttribute to customize the converter on a case-by-case basis, applying it to an entire class, a single property of a class and so on.

I wish I could have the same DX for Custom Type Mappings, something like a SwaggerCustomTypeAttribute that would take a type that provides a Func<OpenApiSchema> or something similar, and then use that information to register it as a type when swagger is generating its information.

Additional context

I managed to implement a similar behavior in my project like so:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class SwaggerCustomTypeAttribute : Attribute
{
    private readonly Type _customTypeProvider;

    public SwaggerCustomTypeAttribute(Type customTypeProvider)
    {
        _customTypeProvider = customTypeProvider;
        ArgumentNullException.ThrowIfNull(customTypeProvider);
        if (customTypeProvider.GetInterface(nameof(ISwaggerCustomTypeProvider)) is null)
            throw new ArgumentException(nameof(customTypeProvider) + "must implement ISwaggerCustomTypeProvider");
    }

    public Func<OpenApiSchema> GetSwaggerCustomTypeProvider()
    {
        var providerClassCtor = _customTypeProvider.GetConstructors()[0];
        var provider = providerClassCtor.Invoke(null) as ISwaggerCustomTypeProvider;
        return provider!.GetCustomType();
    }
}

public interface ISwaggerCustomTypeProvider
{
    Func<OpenApiSchema> GetCustomType();
}

and then I can add [SwaggerCustomType(typeof(ProductIdSwaggerTypeProvider))] to my value object(ProductId in this example), where ProductIdSwaggerTypeProvider : ISwaggerCustomTypeProvider.

Finally, I added the following to my swagger gen options:

foreach (var type in typesToMap)
{
    if (Attribute.GetCustomAttribute(type, typeof(SwaggerCustomTypeAttribute)) is SwaggerCustomTypeAttribute typeProvider)
    {
        options.SchemaGeneratorOptions.CustomTypeMappings.Add(type, typeProvider.GetSwaggerCustomTypeProvider());
    }
}

where typesToMap is a manually written array of Types that I know have the SwaggerCustomTypeAttribute, though I could probably do a more automated version of that by scanning all types in the assembly.

My point is, I wish I could have that attribute and avoid the whole writing down the value objects array, or scanning the assembly for implementing types, or even have the attribute work on properties and/or parameters, in the same way that JsonConverterAttribute works.

All this said, if this feature exists somewhere I failed to find it within the docs so I'd appreciate a direction for that :)

Thank you

@martincostello martincostello added the help-wanted A change up for grabs for contributions from the community label Jul 25, 2024
@martincostello
Copy link
Collaborator

I like this idea - I've done a similar thing myself for custom types for IDs with Minimal APIs, but I've not had to marry it up with Swashbuckle.

Would you consider submitting a PR to add this sort of functionality?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature suggestion help-wanted A change up for grabs for contributions from the community
Projects
None yet
Development

No branches or pull requests

2 participants