I have an Asp.Net web API 5.2 project in c# and generating documentation with Swashbuckle.
I have model that contain inheritance something like having an Animal property from an Animal abstract class and Dog and Cat classes that derive from it.
Swashbuckle only shows the schema for the Animal class so I tried to play with ISchemaFilter (that what they suggest too) but I couldn't make it work and also I cannot find a proper example.
Anybody can help?
It seems Swashbuckle doesn't implement polymorphism correctly and I understand the point of view of the author about subclasses as parameters (if an action expects an Animal class and behaves differently if you call it with a dog object or a cat object, then you should have 2 different actions...) but as return types I believe that it is correct to return Animal and the objects could be Dog or Cat types.
So to describe my API and produce a proper JSON schema in line with correct guidelines (be aware of the way I describe the disciminator, if you have your own discriminator you may need to change that part in particular), I use document and schema filters as follows:
SwaggerDocsConfig configuration; ..... configuration.DocumentFilter<PolymorphismDocumentFilter<YourBaseClass>>(); configuration.SchemaFilter<PolymorphismSchemaFilter<YourBaseClass>>(); ..... public class PolymorphismSchemaFilter<T> : ISchemaFilter { private readonly Lazy<HashSet<Type>> derivedTypes = new Lazy<HashSet<Type>>(Init); private static HashSet<Type> Init() { var abstractType = typeof(T); var dTypes = abstractType.Assembly .GetTypes() .Where(x => abstractType != x && abstractType.IsAssignableFrom(x)); var result = new HashSet<Type>(); foreach (var item in dTypes) result.Add(item); return result; } public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type) { if (!derivedTypes.Value.Contains(type)) return; var clonedSchema = new Schema { properties = schema.properties, type = schema.type, required = schema.required }; //schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in SwashBuckle var parentSchema = new Schema { @ref = "#/definitions/" + typeof(T).Name }; schema.allOf = new List<Schema> { parentSchema, clonedSchema }; //reset properties for they are included in allOf, should be null but code does not handle it schema.properties = new Dictionary<string, Schema>(); } } public class PolymorphismDocumentFilter<T> : IDocumentFilter { public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, System.Web.Http.Description.IApiExplorer apiExplorer) { RegisterSubClasses(schemaRegistry, typeof(T)); } private static void RegisterSubClasses(SchemaRegistry schemaRegistry, Type abstractType) { const string discriminatorName = "discriminator"; var parentSchema = schemaRegistry.Definitions[SchemaIdProvider.GetSchemaId(abstractType)]; //set up a discriminator property (it must be required) parentSchema.discriminator = discriminatorName; parentSchema.required = new List<string> { discriminatorName }; if (!parentSchema.properties.ContainsKey(discriminatorName)) parentSchema.properties.Add(discriminatorName, new Schema { type = "string" }); //register all subclasses var derivedTypes = abstractType.Assembly .GetTypes() .Where(x => abstractType != x && abstractType.IsAssignableFrom(x)); foreach (var item in derivedTypes) schemaRegistry.GetOrRegister(item); } }
What the previous code implements is specified here, in the section "Models with Polymorphism Support. It basically produces something like the following:
{ "definitions": { "Pet": { "type": "object", "discriminator": "petType", "properties": { "name": { "type": "string" }, "petType": { "type": "string" } }, "required": [ "name", "petType" ] }, "Cat": { "description": "A representation of a cat", "allOf": [ { "$ref": "#/definitions/Pet" }, { "type": "object", "properties": { "huntingSkill": { "type": "string", "description": "The measured skill for hunting", "default": "lazy", "enum": [ "clueless", "lazy", "adventurous", "aggressive" ] } }, "required": [ "huntingSkill" ] } ] }, "Dog": { "description": "A representation of a dog", "allOf": [ { "$ref": "#/definitions/Pet" }, { "type": "object", "properties": { "packSize": { "type": "integer", "format": "int32", "description": "the size of the pack the dog is from", "default": 0, "minimum": 0 } }, "required": [ "packSize" ] } ] } } }
To follow on from Paulo's great answer, if you're using Swagger 2.0, you'll need to modify the classes as shown:
public class PolymorphismSchemaFilter<T> : ISchemaFilter { private readonly Lazy<HashSet<Type>> derivedTypes = new Lazy<HashSet<Type>>(Init); private static HashSet<Type> Init() { var abstractType = typeof(T); var dTypes = abstractType.Assembly .GetTypes() .Where(x => abstractType != x && abstractType.IsAssignableFrom(x)); var result = new HashSet<Type>(); foreach (var item in dTypes) result.Add(item); return result; } public void Apply(Schema model, SchemaFilterContext context) { if (!derivedTypes.Value.Contains(context.SystemType)) return; var clonedSchema = new Schema { Properties = model.Properties, Type = model.Type, Required = model.Required }; //schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in SwashBuckle var parentSchema = new Schema { Ref = "#/definitions/" + typeof(T).Name }; model.AllOf = new List<Schema> { parentSchema, clonedSchema }; //reset properties for they are included in allOf, should be null but code does not handle it model.Properties = new Dictionary<string, Schema>(); } } public class PolymorphismDocumentFilter<T> : IDocumentFilter { private static void RegisterSubClasses(ISchemaRegistry schemaRegistry, Type abstractType) { const string discriminatorName = "discriminator"; var parentSchema = schemaRegistry.Definitions[abstractType.Name]; //set up a discriminator property (it must be required) parentSchema.Discriminator = discriminatorName; parentSchema.Required = new List<string> { discriminatorName }; if (!parentSchema.Properties.ContainsKey(discriminatorName)) parentSchema.Properties.Add(discriminatorName, new Schema { Type = "string" }); //register all subclasses var derivedTypes = abstractType.Assembly .GetTypes() .Where(x => abstractType != x && abstractType.IsAssignableFrom(x)); foreach (var item in derivedTypes) schemaRegistry.GetOrRegister(item); } public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context) { RegisterSubClasses(context.SchemaRegistry, typeof(T)); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With