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 prop
I'd like to follow up on Craig's answer.
If you use NSwag to generate TypeScript definitions from the Swagger API documentation generated with Swashbuckle (3.x at the time of writing) using the method explained in Paulo's answer and further enhanced in Craig's answer you will probably face the following problems:
Generated TypeScript definitions will have duplicate properties even though the generated classes will extend the base classes. Consider the following C# classes:
public abstract class BaseClass
{
public string BaseProperty { get; set; }
}
public class ChildClass : BaseClass
{
public string ChildProperty { get; set; }
}
When using the aforementioned answers, the resulting TypeScript definition of IBaseClass
and IChildClass
interfaces would look like this:
export interface IBaseClass {
baseProperty : string | undefined;
}
export interface IChildClass extends IBaseClass {
baseProperty : string | undefined;
childProperty: string | undefined;
}
As you can see, the baseProperty
is incorrectly defined in both base and child classes. To solve this, we can modify the Apply
method of the PolymorphismSchemaFilter
class to include only owned properties to the schema, i.e. to exclude the inherited properties from the current types schema. Here is an example:
public void Apply(Schema model, SchemaFilterContext context)
{
...
// Prepare a dictionary of inherited properties
var inheritedProperties = context.SystemType.GetProperties()
.Where(x => x.DeclaringType != context.SystemType)
.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
var clonedSchema = new Schema
{
// Exclude inherited properties. If not excluded,
// they would have appeared twice in nswag-generated typescript definition
Properties =
model.Properties.Where(x => !inheritedProperties.ContainsKey(x.Key))
.ToDictionary(x => x.Key, x => x.Value),
Type = model.Type,
Required = model.Required
};
...
}
Generated TypeScript definitions will not reference properties from any existing intermediate abstract classes. Consider the following C# classes:
public abstract class SuperClass
{
public string SuperProperty { get; set; }
}
public abstract class IntermediateClass : SuperClass
{
public string IntermediateProperty { get; set; }
}
public class ChildClass : BaseClass
{
public string ChildProperty { get; set; }
}
In this case, the generated TypeScript definitions would look like this:
export interface ISuperClass {
superProperty: string | undefined;
}
export interface IIntermediateClass extends ISuperClass {
intermediateProperty : string | undefined;
}
export interface IChildClass extends ISuperClass {
childProperty: string | undefined;
}
Notice how the generated IChildClass
interface extends ISuperClass
directly, ignoring the IIntermediateClass
interface, effectively leaving any instance of IChildClass
without the intermediateProperty
property.
We can use the following code to solve this problem:
public void Apply(Schema model, SchemaFilterContext context)
{
...
// Use the BaseType name for parentSchema instead of typeof(T),
// because we could have more classes in the hierarchy
var parentSchema = new Schema
{
Ref = "#/definitions/" + (context.SystemType.BaseType?.Name ?? typeof(T).Name)
};
...
}
This will ensure that the child class correctly references the intermediate class.
In conclusion, the final code would then look like this:
public void Apply(Schema model, SchemaFilterContext context)
{
if (!derivedTypes.Value.Contains(context.SystemType))
{
return;
}
// Prepare a dictionary of inherited properties
var inheritedProperties = context.SystemType.GetProperties()
.Where(x => x.DeclaringType != context.SystemType)
.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
var clonedSchema = new Schema
{
// Exclude inherited properties. If not excluded,
// they would have appeared twice in nswag-generated typescript definition
Properties =
model.Properties.Where(x => !inheritedProperties.ContainsKey(x.Key))
.ToDictionary(x => x.Key, x => x.Value),
Type = model.Type,
Required = model.Required
};
// Use the BaseType name for parentSchema instead of typeof(T),
// because we could have more abstract classes in the hierarchy
var parentSchema = new Schema
{
Ref = "#/definitions/" + (context.SystemType.BaseType?.Name ?? typeof(T).Name)
};
model.AllOf = new List { parentSchema, clonedSchema };
// reset properties for they are included in allOf, should be null but code does not handle it
model.Properties = new Dictionary();
}