Проблема поддержки полиморфизма в Swashbuckle
Я использую Swashbuckle v3.0.
Я не уверен, что это ошибка или нет, но полиморфизм работает не так, как должен. У меня есть следующие классы:
BasePersonDocumentDto
{
Id,
Number
}
IdentityDto: BasePersonDocumentDto
{
}
PassportDto: BasePersonDocumentDto
{
VisaNumber
}
Чтобы применить наследование и полиморфизм, я создал схему и фильтры документов. Я следовал за этим ответом Ниже приведен код, который я использовал.
public class PolymorphismSchemaFilter<T> : ISchemaFilter
{
private List<Type> derivedTypes = new List<Type>() { typeof(IdentityDto), typeof(PassportDto) };
public void Apply(Schema model, SchemaFilterContext context)
{
if (!derivedTypes.Contains(context.SystemType)) return;
var baseSchema = new Schema() { Ref = "#/definitions/" + typeof(T).Name };
var clonedBaseSchema = new Schema
{
Properties = model.Properties,
Type = model.Type,
Required = model.Required
};
model.AllOf = new List<Schema> { baseSchema, clonedBaseSchema };
//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 List<Type> derivedTypes = new List<Type>() { typeof(IdentityDto), typeof(PassportDto) };
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
const string discriminatorName = "discriminator";
var baseSchema = context.SchemaRegistry.Definitions[typeof(T).Name];
//Discriminator property
baseSchema.Discriminator = discriminatorName;
baseSchema.Required = new List<string> { discriminatorName };
if (!baseSchema.Properties.ContainsKey(discriminatorName))
baseSchema.Properties.Add(discriminatorName, new Schema { Type = "string" });
//Register dervied classes
foreach (var item in derivedTypes)
context.SchemaRegistry.GetOrRegister(item);
}
}
Сгенерированные определения:
//This is for BasePersonDocumentDto and for IdentityDto
"BasePersonDocumentDto":{
"required":[
"discriminator"
],
"type":"object",
"properties":{
"number":{
"type":"string"
},
"id":{
"format":"int32",
"type":"integer"
},
"discriminator":{
"type":"string"
}
},
"discriminator":"discriminator"
}
"PassportDto":{
"required":[
"discriminator"
],
"type":"object",
"properties":{
"number":{
"type":"string"
},
"id":{
"format":"int32",
"type":"integer"
},
"visaNumber":{
"type":"string"
},
"discriminator":{
"type":"string"
}
},
"discriminator":"discriminator"
}
Теперь, чтобы проверить, я создаю InputDto, который имеет свойство List. Я попытался передать следующий JSON, но оба переданных элемента отображаются как BasePersonDocumentDto, даже если дискриминатор передан правильно. Вот JSON, который я передал:
{
"documentsDto":[
{
"discriminator":"IdentityDto"
},
{
"visaNumber":"RRXX323TR",
"discriminator":"PassportDto"
}
]
}
1 ответ
Через некоторое время я смог найти способ решить мою проблему. Мой случай был очень прост. У меня просто была база и несколько производных классов, но swagger генерировал только базу, но не производную.
Чтобы решить эту проблему, я использовал код, который я разместил в своем вопросе, который применяет наследование, а также добавляет неявный столбец с именем "дискриминатор". Этот столбец используется для различения подтипов.
Последняя проблема заключалась в том, что при десериализации объекта объект swagger не смог использовать столбец "дискриминатор", и в результате он всегда соответствовал базовому типу. Для решения этой проблемы я нашел два решения: первое - реализовать пользовательский механизм связывания моделей, а второе - реализовать собственный JsonConverter.
Я выбрал второй вариант, так как он кажется более логичным, поскольку моя проблема была связана с десериализацией Json. Наряду с использованием JsonConverter с моим базовым классом, я также украсил его атрибутом "KnownType".
[KnownType(typeof(IdentityDto))]
[KnownType(typeof(PassportDto))]
[JsonConverter(typeof(InheritanceConverter))]
BasePersonDocumentDto
{
Id,
Number
}
Ниже приведен код для JsonConverter:
/// <summary>
/// Json deserialization for inheritance structure with discriminator
/// </summary>
public class InheritanceConverter : JsonConverter
{
/// <summary>
/// Default name for the discriminator property
/// </summary>
private string _discriminator { get; set; } = "discriminator";
public InheritanceConverter()
{
}
/// <summary>
/// Discriminator name to map between types
/// </summary>
/// <param name="discriminator">The discriminator property name. The defualt value is 'discriminator'</param>
public InheritanceConverter(string discriminator)
{
if (!string.IsNullOrWhiteSpace(discriminator))
_discriminator = discriminator;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//Check null
if (reader.TokenType == JsonToken.Null) return null;
//Parse json
var jDerivedObject = JObject.Load(reader);
//Get discriminator
var discriminator = jDerivedObject.Value<string>(_discriminator);
if (string.IsNullOrWhiteSpace(discriminator))
throw new Exception("Invalid discriminator value");
//Get the type
var derivedType = GetSubType(discriminator, objectType);
//Create a new instance of the target type
var derivedObject = Activator.CreateInstance(derivedType);
//Populate the derived object
serializer.Populate(jDerivedObject.CreateReader(), derivedObject);
return derivedObject;
}
//TODO:- validate based on the base and sub-types via the KnownTypeAttributes
public override bool CanConvert(Type objectType) => true;
//Not required
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
//Not required
public override bool CanWrite => false;
#region Methods
/// <summary>
/// Get sub-type via KnownTypeAttributes
/// </summary>
/// <param name="derivedTypeName">The target type name which corosponds to the discriminator</param>
private Type GetSubType(string derivedTypeName, Type baseType)
{
var knownTypes = baseType.GetCustomAttributes(false).Where(ca => ca.GetType().Name == "KnownTypeAttribute").ToList();
if (knownTypes == null || knownTypes.Count == 0)
throw new Exception(
string.Format("Couldn't find any KnownAttributes over the base {0}. Please define at least one KnownTypeAttribute to determine the sub-type", baseType.Name));
foreach (dynamic type in knownTypes)
{
if (type.Type != null && type.Type.Name.ToLower() == derivedTypeName.ToLower())
return type.Type;
}
throw new Exception(string.Format("Discriminator '{0}' doesn't match any of the defined sub-types via KnownTypeAttributes", derivedTypeName));
}
#endregion
}