Swashbuckle.aspnetcore: Swagger Generation not respecting overrided schema for type (via MapType<T>)

Created on 21 Feb 2017  路  8Comments  路  Source: domaindrivendev/Swashbuckle.AspNetCore

Hello!

I have an Id type that consists in something like <resource-type>_<guid> that is known by it's string form, like aspnet_00995ee0f9804f2f88eaa99f468023e5. I made an specific JsonConverter to work with it and it's serializing and deserializing like a charm. I'm using this type as my id route parameter (from the path portion ["{controller}/{action}/{id}" is my default route]) instead of the common MVC string id.

I'm overriding this specific type to be seen as a string, as presented here.

services.AddSwaggerGen(options =>
{
    options.MapType<Id>(() => new Schema { Pattern = "^[a-z0-9]+_[a-f0-9]{32}$", Type = "string" });
});

The generated paths that are using this type as a parameter in the path portion are not being properly described, as seen below:

...
"/websites/{id}": {
  "get": {
    "tags": ["Websites"],
    "operationId": "WebsitesByIdGet",
    "consumes": [],
    "produces": ["text/plain", "application/json", "text/json"],
    "parameters": [{
      "name": "Guid",
      "in": "path",
      "required": true,
      "type": "string"
    }, {
      "name": "ResourceType",
      "in": "path",
      "required": true,
      "type": "string"
    }, {
      "name": "Value",
      "in": "path",
      "required": true,
      "type": "string"
    }, {
      "name": "id",
      "in": "path",
      "required": true,
      "type": "string"
    }],
...

The Guid, ResourceType and Value parameters are, in fact, properties of my Id class. After listing these unwanted parameters (because I already set that I want the Id type to be treated as a simple string), it ends with the _almost correct_ id parameter definition (it lacks the pattern I set on my specific Schema for this type).

It should be only listing

[{
  "name": "id",
  "in": "path",
  "required": true,
  "type": "string",
  "pattern": "^[a-z0-9]+_[a-f0-9]{32}$"
}]

here, right?

When this Id type of mine is used as a property of another type (like in the website type), the definitions portion of the document shows the Id type definition correct:

...
"api.company.com.models.website": {
  "type": "object",
  "properties": {
    "type": {
      "type": "string",
      "readOnly": true
    },
    "id": {
      "pattern": "^[a-z0-9]+_[a-f0-9]{32}$",
      "type": "string"
    },
    "identifier": {
      "type": "string"
    }
  }
}
...

Let me know if you need more details or if there's something I can do to help in that.
swagger.json

Most helpful comment

I've stumbled upon the same issue in connection with NodaTime's LocalDate (probably same for other NodaTime types).

Has there been any support implemented to configure whether Query parameters should be expanded?

All 8 comments

Hmmm - see the referenced ticket above in the MVC repo. Swashbuckle relies heavily on _ApiExplorer_, the API metadata layer that ships with ASP.NET Core. The root cause of your issue lies in the fact that _ApiExplorer_ automatically expands complex types into multiple parameters for all-but-body parameters before Swashbuckle takes over. I've started further discussion in that issue around the merits of this behavior and it's impact to downstream libraries like SB but I'm not sure exactly what the best course of action would be.

For your case, I'm trying to think of some workaround (there's always a workaround :)) and will get back to you if anything comes to mind.

While I'm thinking about this, I just have another quick question ...

So, you've implemented a JsonConverter which will serialize/deserialize to/from a string for any request and response payloads which involve this type. However, in the cases when you're using it as a "path" parameter, how are you having ASP.NET Core convert from the path string into the complex type parameter?

[HttpGet("websites/{id}")]
public Website GetById(Id id) // how are you binding "/websites/aspnet_foo" to the Id parameter value?
{
...
}

@domaindrivendev thank you for the quick action.

I'm happy to see your comments about this on the aspnet/Mvc related issue. Subscribing now.

About your last question (binding string to complex type): I just implemented an Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder (and a IModelBinderProvider).

    public class IdModelBinderProvider : IModelBinderProvider
    {
        public static readonly Type CoveredType = typeof(Id);

        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context.Metadata.IsComplexType &&
                context.Metadata.ModelType == CoveredType)
                return new IdModelBinder();

            return null;
        }
    }
    public class IdModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var value = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);

            Id id = null;
            new Action(() => id = Id.Parse(value.FirstValue))
                .ReThrowAs(message => new ExpectedErrorException(message, StatusCodes.BadRequest));

            bindingContext.Result = ModelBindingResult.Success(id);

            return Task.CompletedTask;
        }
    }

And setting it on Startup.ConfigureServices(IServiceCollection):

services.AddMvcCore(options =>
{
    ...
    options.ModelBinderProviders.Add(new IdModelBinderProvider());
    ...
}

One way to get what you want would be to write a TypeConverter for your Id type that can convert from string. https://msdn.microsoft.com/en-us/library/ayybcxe5.aspx If you do this instead we will not only stop expanding the properties of this type, but you won't need a custom model binder for it either.

We currently decide whether or not a type is 'simple' based on whether or not it has a type converter that can convert from string. I think in the future we'll want to make it trivial to configure whether a not a type is 'simple' for API explorer purposes, but for right now this is what you can do.

Thank you @rynowak.

The funny part is that at first I've implemented a TypeConverter, but then I realized that the AspNet way of dealing with this kind of conversion/binding is more into the IModelBinder side, so I changed my code to the IModelBinder taste (at that moment I was not using Swashbuckle yet). Just changed it back to the IdTypeConverter and it worked as you have predicted.

...
"/websites/{id}": {
  "get": {
    "tags": ["Websites"],
    "operationId": "WebsitesByIdGet",
    "consumes": [],
    "produces": ["text/plain", "application/json", "text/json"],
    "parameters": [{
      "name": "id",
      "in": "path",
      "required": true,
      "type": "string",
      "pattern": "^[a-z0-9]+_[a-f0-9]{32}$"
    }]
...

Thank you for the tip and thanks also to @domaindrivendev for referencing the right issue on the aspnet/Mvc repo and standing for it.

I've logged https://github.com/aspnet/Mvc/issues/5850 to follow up on this as well. Building a TypeConverter is what will work right now, we need a better long term solution

Building a TypeConverter is what will work right now, we need a better long term solution

Exactly. The TypeConverter is the .NET way, but the API Explorer needs to understand the AspNet way of dealing with this (IModelBinder use).

I've stumbled upon the same issue in connection with NodaTime's LocalDate (probably same for other NodaTime types).

Has there been any support implemented to configure whether Query parameters should be expanded?

Was this page helpful?
0 / 5 - 0 ratings