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

SwaggerDiscriminator Attribute: discriminator field moved from paths to components #2052

Open
danstur opened this issue Mar 10, 2021 · 11 comments · May be fixed by #2683
Open

SwaggerDiscriminator Attribute: discriminator field moved from paths to components #2052

danstur opened this issue Mar 10, 2021 · 11 comments · May be fixed by #2683

Comments

@danstur
Copy link

danstur commented Mar 10, 2021

After updating from 5.6.3 to 6.x (tested both 6.1.0 and 6.0.7) when using the SwaggerDiscriminator attribute the "discriminator" field moved from paths/<path>/responses/<..>/content/schema into /components/schemas/BaseType.

I.e. in the old code we had:

"responses": {
  "200": {
	"content": {
	  "application/json": {
		"schema": {
		  "oneOf": [
			{
			  "$ref": "#/components/schemas/Foo"
			},
			{
			  "$ref": "#/components/schemas/Bar"
			}
		  ],
		  "discriminator": {
			"propertyName": "type",
			"mapping": {
			  "Foo": "#/components/schemas/Foo",
			  "Bar": "#/components/schemas/Bar"
			}
		  }
		}
	  }
	}
  }
}

now this has become

"responses": {
  "200": {
	"content": {
	  "application/json": {
		"schema": {
		  "oneOf": [
			{
			  "$ref": "#/components/schemas/Foo"
			},
			{
			  "$ref": "#/components/schemas/Bar"
			}
		  ]
		}
	  }
	}
  }
}
// components
  "FooBase": {
	"required": [
	  "type"
	],
	"type": "object",
	"properties": {
	  "type": {
		"type": "string",
		"nullable": true,
		"readOnly": true
	  }
	},
	"additionalProperties": false,
	"discriminator": {
	  "propertyName": "type",
	  "mapping": {
		"Foo": "#/components/schemas/Foo",
		"Bar": "#/components/schemas/Bar"
	  }
	}
  }

and the discriminator and mapping can only be found under components of the base type. I assume this is an intentional change?

I can see the point of not repeating the discriminator mappings repeatedly, but it seems rather cumbersome to find the discriminator and mapping with the new structure (basically find the common allOf() parent of all the types referenced in oneOf I think? Not sure this would work for more complicated inheritance hierarchies though).

The sample shown at swagger.io has the discriminator under the response part as it was done in the old version though.

Reproduction

Using the basic ASP.NET Core 3.1 template with the following controller:

    [ApiController]
    [Route("[controller]")]
    public class LogsController : ControllerBase
    {
        [HttpGet]
        [ProducesResponseType(typeof(FooBase), StatusCodes.Status200OK)]
        public FooBase Get()
        {
            return new Foo();
        }
    }

    [SwaggerDiscriminator("type")]
    [SwaggerSubType(typeof(Foo), DiscriminatorValue = nameof(Foo))]
    [SwaggerSubType(typeof(Bar), DiscriminatorValue = nameof(Bar))]
    public abstract class FooBase
    {
        public string Type { get; }

        public FooBase(string type)
        {
            Type = type;
        }
    }

    public class Foo : FooBase
    {
        public Foo() : base(nameof(Foo)) { }
    }

    public class Bar : FooBase
    {
        public Bar() : base(nameof(Bar)) { }
    }

and those changes added to the Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
        // standard template 
	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1",
			new OpenApiInfo
			{
				Title = "My API - V1",
				Version = "v1"
			}
		);
		c.EnableAnnotations(true, true);
	});
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
        // standard template 
	app.UseSwagger();
	app.UseSwaggerUI(c =>
	{
		c.SwaggerEndpoint("v1/swagger.json", "My API V1");
	});
}

results in different swagger.jsons depending on whether 5.6.3 or 6.1.0 is used.

Swagger.json 5.6.3
{
  "openapi": "3.0.1",
  "info": {
    "title": "My API - V1",
    "version": "v1"
  },
  "paths": {
    "/Logs": {
      "get": {
        "tags": [
          "Logs"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "text/plain": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Foo"
                    },
                    {
                      "$ref": "#/components/schemas/Bar"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "type",
                    "mapping": {
                      "Foo": "#/components/schemas/Foo",
                      "Bar": "#/components/schemas/Bar"
                    }
                  }
                }
              },
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Foo"
                    },
                    {
                      "$ref": "#/components/schemas/Bar"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "type",
                    "mapping": {
                      "Foo": "#/components/schemas/Foo",
                      "Bar": "#/components/schemas/Bar"
                    }
                  }
                }
              },
              "text/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Foo"
                    },
                    {
                      "$ref": "#/components/schemas/Bar"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "type",
                    "mapping": {
                      "Foo": "#/components/schemas/Foo",
                      "Bar": "#/components/schemas/Bar"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Bar": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/FooBase"
          }
        ],
        "additionalProperties": false
      },
      "FooBase": {
        "required": [
          "type"
        ],
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true,
            "readOnly": true
          }
        },
        "additionalProperties": false
      },
      "Foo": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/FooBase"
          }
        ],
        "additionalProperties": false
      }
    }
  }
}
Swagger.json 6.1.0
{
  "openapi": "3.0.1",
  "info": {
    "title": "My API - V1",
    "version": "v1"
  },
  "paths": {
    "/Logs": {
      "get": {
        "tags": [
          "Logs"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "text/plain": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Foo"
                    },
                    {
                      "$ref": "#/components/schemas/Bar"
                    }
                  ]
                }
              },
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Foo"
                    },
                    {
                      "$ref": "#/components/schemas/Bar"
                    }
                  ]
                }
              },
              "text/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Foo"
                    },
                    {
                      "$ref": "#/components/schemas/Bar"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Bar": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/FooBase"
          }
        ],
        "additionalProperties": false
      },
      "Foo": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/FooBase"
          }
        ],
        "additionalProperties": false
      },
      "FooBase": {
        "required": [
          "type"
        ],
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true,
            "readOnly": true
          }
        },
        "additionalProperties": false,
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "Foo": "#/components/schemas/Foo",
            "Bar": "#/components/schemas/Bar"
          }
        }
      }
    }
  }
}
@danstur
Copy link
Author

danstur commented May 10, 2021

@domaindrivendev It's rather hard to pinpoint when exactly this change was introduced, but I think it might be #1983 .

I looked through the whole history and don't find any discussion of this change. It'd be really helpful if I could find some reasoning behind this change and how exactly clients should deal with the new schema.

@arutkowski00
Copy link

Is there any update regarding this issue? My client generator started producing invalid code after the update...

@collinstevens
Copy link

collinstevens commented Apr 21, 2022

I don't think this change violates the specification, but this at least breaks NSwag client generation.

Without the discriminator property defined on the schema object, NSwag will simply generate the using the first schema object or reference object in the oneOf array.

The specification states there is a discriminator field on the schema object and isn't specific as to when it is required or optional.

It would make sense to generate the discriminator field if the user is returning a polymorphic type, as the previous versions had done before.

https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/
https://swagger.io/specification/#schema-object

The following code is present in v5.6.3, but is then removed by v6.0.0.

var schema = new OpenApiSchema
{
OneOf = new List<OpenApiSchema>(),
Discriminator = TryGetDiscriminatorName(dataContract, out string discriminatorName)
? new OpenApiDiscriminator { PropertyName = discriminatorName }
: null
};

return new OpenApiSchema
{
OneOf = knownTypesDataContracts
.Select(allowedTypeDataContract => GenerateConcreteSchema(allowedTypeDataContract, schemaRepository))
.ToList()
};

This is the offending commit bfa6429.

Pulling down the master branch, I was naively able to re-add the removed lines, but I don't know why it was removed to begin with. @domaindrivendev would you be able to chime in as it appears you removed the original code?

@rofenix2
Copy link

rofenix2 commented Mar 1, 2023

This issue is breaking the openapi typescript fetch client generator and others. Any news on this?.

@marnilss
Copy link

I'm also interested in a solution to this - NSwag doesn't know how to apply the JsonInheritanceAttribute with the correct mappings anymore.

@danielcrabtree
Copy link

As other have alluded to, I believe this change breaks the spec.

As per the swagger documentation: https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/ the discriminator name and mapping belong on the polymorphic type as it was previously.

Putting the discriminator name on the subtypes is somewhat meaningless, as it is the polymorphic type that determines which field it wants to use on the underlying subtypes. Only the discriminator property belongs on the underlying subtypes.

I can imagine a scenario where you have some overlap between two different polymorphic hierarchies with some common subtypes in both. Each hierarchy may use a different discriminator name operating over the same types and therefore, the discriminator name must be on the polymorphic type and not the subtype.

This change in behavior should be reverted.

@danielcrabtree danielcrabtree linked a pull request Jul 15, 2023 that will close this issue
@danielcrabtree
Copy link

I have submitted a Pull Request #2683 that fixes this bug.

@mrcrowl
Copy link

mrcrowl commented Aug 9, 2023

Is there a known workaround for this bug?

@Scrambles56
Copy link

Scrambles56 commented Aug 9, 2023

A workaround that I used for this, using Swashbuckle.AspNetCore@6.5.0 was the following:

services.AddSwaggerGen(opts =>
{
    opts.UseOneOfForPolymorphism();
    opts.SelectDiscriminatorNameUsing(_ => "$type");
});

It adds a property to polymorphic types:

"blahtype": {
    "properties": {
          "$type": {
              "type": "string"
          },
      }
}

@rofenix2
Copy link

rofenix2 commented Nov 22, 2023

So did this ever got fixed? i am using a custom OperationFilter for now...

@whschultz
Copy link

A workaround that I used for this, using Swashbuckle.AspNetCore@6.5.0 was the following:

services.AddSwaggerGen(opts =>
{
    opts.UseOneOfForPolymorphism();
    opts.SelectDiscriminatorNameUsing(_ => "$type");
});

It adds a property to polymorphic types:

"blahtype": {
    "properties": {
          "$type": {
              "type": "string"
          },
      }
}

I expanded on this and am using the .NET Serialization attributes to fill out the correct information. This gives me the descriminator and its mapping in the output schema, all customizable with JsonDerivedType and JsonPolymorphic attributes added to the base class.

// using System.Text.Json.Serialization;

                opts.SelectDiscriminatorNameUsing(
                    _ =>
                    {
                        if (Attribute.GetCustomAttribute(_, typeof(JsonPolymorphicAttribute))
                            is JsonPolymorphicAttribute polymorphic)
                            return polymorphic.TypeDiscriminatorPropertyName;

                        return null;
                    }
                );
                // Use the JsonDerivedTypeAttribute on the base class to determine 
                // the discriminator value for the derived class.
                opts.SelectDiscriminatorValueUsing(
                    _ =>
                    {
                        var baseType = _;

                        string? output = null;

                        while (baseType != null && output == null)
                        {
                            output =
                                Attribute
                                    .GetCustomAttributes(baseType, typeof(JsonDerivedTypeAttribute), inherit: true)
                                    .Cast<JsonDerivedTypeAttribute>()
                                    .Where(a => a.DerivedType == _.UnderlyingSystemType)
                                    .Select(a => a.TypeDiscriminator?.ToString())
                                    .Where(d => d != null)
                                    .Distinct()
                                    .SingleOrDefault();

                            baseType = baseType.BaseType;
                        }

                        return output;
                    }
                );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants