J'essaie de migrer de Newtonsoft.Json vers System.Text.Json. Je veux désérialiser une classe abstraite. Newtonsoft.Json a TypeNameHandling pour cela. Existe-t-il un moyen de désérialiser une classe abstraite via System.Text.Json en .net core 3.0 ?
Réponses
Trop de publicités?C'est mon JsonConverter pour tous les types abstraits :
private class AbstractClassConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("JsonTokenType.StartObject not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "$type")
throw new JsonException("Property $type not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.String)
throw new JsonException("Value at $type is invalid.");
string assemblyQualifiedName = reader.GetString();
var type = Type.GetType(assemblyQualifiedName);
using (var output = new MemoryStream())
{
ReadObject(ref reader, output, options);
return JsonSerializer.Deserialize(output.ToArray(), type, options);
}
}
private void ReadObject(ref Utf8JsonReader reader, Stream output, JsonSerializerOptions options)
{
using (var writer = new Utf8JsonWriter(output, new JsonWriterOptions
{
Encoder = options.Encoder,
Indented = options.WriteIndented
}))
{
writer.WriteStartObject();
var objectIntend = 0;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.None:
case JsonTokenType.Null:
writer.WriteNullValue();
break;
case JsonTokenType.StartObject:
writer.WriteStartObject();
objectIntend++;
break;
case JsonTokenType.EndObject:
writer.WriteEndObject();
if(objectIntend == 0)
{
writer.Flush();
return;
}
objectIntend--;
break;
case JsonTokenType.StartArray:
writer.WriteStartArray();
break;
case JsonTokenType.EndArray:
writer.WriteEndArray();
break;
case JsonTokenType.PropertyName:
writer.WritePropertyName(reader.GetString());
break;
case JsonTokenType.Comment:
writer.WriteCommentValue(reader.GetComment());
break;
case JsonTokenType.String:
writer.WriteStringValue(reader.GetString());
break;
case JsonTokenType.Number:
writer.WriteNumberValue(reader.GetInt32());
break;
case JsonTokenType.True:
case JsonTokenType.False:
writer.WriteBooleanValue(reader.GetBoolean());
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
writer.WriteStartObject();
var valueType = value.GetType();
var valueAssemblyName = valueType.Assembly.GetName();
writer.WriteString("$type", $"{valueType.FullName}, {valueAssemblyName.Name}");
var json = JsonSerializer.Serialize(value, value.GetType(), options);
using (var document = JsonDocument.Parse(json, new JsonDocumentOptions
{
AllowTrailingCommas = options.AllowTrailingCommas,
MaxDepth = options.MaxDepth
}))
{
foreach (var jsonProperty in document.RootElement.EnumerateObject())
jsonProperty.WriteTo(writer);
}
writer.WriteEndObject();
}
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.IsAbstract && !EnumerableInterfaceType.IsAssignableFrom(typeToConvert);
}
N'écrivez pas comme ça
public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}
Si votre classe contient la propriété baseClass, vous la désérialisez comme la baseClass. Si votre baseClass est abstraite et contient la propriété baseClass, vous obtenez une exception.
C'est plus sûr d'écrire comme ça :
public class BaseClass
{
public int Int { get; set; }
}
public class DerivedA : BaseClass
{
public string Str { get; set; }
public BaseClass derived { get; set; }
}
public class DerivedB : BaseClass
{
public bool Bool { get; set; }
public BaseClass derived { get; set; }
}
public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}
public override bool CanConvert(Type type)
{
return typeof(BaseClass) == type;
}
public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA), options);
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB), options);
break;
case TypeDiscriminator.BaseClass:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (BaseClass)JsonSerializer.Deserialize(ref reader, typeof(BaseClass));
break;
default:
throw new NotSupportedException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA, options);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB, options);
}
else if (value is BaseClass baseClass)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.BaseClass);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, baseClass);
}
else
{
throw new NotSupportedException();
}
writer.WriteEndObject();
}
}
Mais votre BaseClass ne doit pas contenir de propriété avec le type BaseClass ou inheritor.
Pour la désérialisation des propriétés d'interface, j'ai créé un simple convertisseur StaticTypeMapConverter
public class StaticTypeMapConverter<SourceType, TargetType> : JsonConverter<SourceType>
where SourceType : class
where TargetType : class, new()
{
public override SourceType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
var jsonObject = jsonDocument.RootElement.GetRawText();
var result = (SourceType)JsonSerializer.Deserialize(jsonObject, typeof(TargetType), options);
return result;
}
}
public override void Write(Utf8JsonWriter writer, SourceType value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
Vous pouvez l'utiliser comme ceci :
var jsonSerializerOptions = new JsonSerializerOptions()
{
Converters = {
new StaticTypeMapConverter<IMyInterface, MyImplementation>(),
new StaticTypeMapConverter<IMyInterface2, MyInterface2Class>(),
},
WriteIndented = true
};
var config = JsonSerializer.Deserialize<Config>(configContentJson, jsonSerializerOptions);
J'ai changé quelques trucs en me basant sur ahsonkhan La réponse de la Commission.
Personnellement, j'aime cette méthode car le client peut simplement donner son objet au serveur. Cependant, la propriété 'Type' doit être la première dans l'objet.
Classe de base et classes dérivées :
public interface IBaseClass
{
public DerivedType Type { get; set; }
}
public class DerivedA : IBaseClass
{
public DerivedType Type => DerivedType.DerivedA;
public string Str { get; set; }
}
public class DerivedB : IBaseClass
{
public DerivedType Type => DerivedType.DerivedB;
public bool Bool { get; set; }
}
private enum DerivedType
{
DerivedA = 0,
DerivedB = 1
}
Vous pouvez créer JsonConverter<IBaseClass>
qui lit et vérifie la propriété "Type" lors de la sérialisation. Il s'en servira pour déterminer le type à désérialiser. Le lecteur doit être copié puisque nous lisons la première propriété comme étant le type. Ensuite, nous devons relire l'objet complet (le passer à la méthode Deserialize).
public class BaseClassConverter : JsonConverter<IBaseClass>
{
public override IBaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
// Creating a copy of the reader (The derived deserialisation has to be done from the start)
Utf8JsonReader typeReader = reader;
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
IBaseClass baseClass = default;
DerivedType type= (DerivedType)reader.GetInt32();
switch (type)
{
case DerivedType.DerivedA:
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case DerivedType.DerivedB:
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
IBaseClass value,
JsonSerializerOptions options)
{
switch(value)
{
case DerivedA derivedA:
JsonSerializer.Serialize(writer, derivedA, options);
break;
case DerivedB derivedB:
JsonSerializer.Serialize(writer, derivedB, options);
break;
default:
throw new NotSupportedException();
}
}
}
Le client est maintenant capable d'envoyer des objets comme suit :
// DerivedA
{
"Type": 0,
"Str": "Hello world!"
}
// DerivedB
{
"Type": 1,
"Bool": false
}
EDITAR:
Modification de la méthode Read pour pouvoir traiter le nom de la propriété qui n'est pas dans le premier ordre. Maintenant, il lit simplement à travers le json et s'arrête jusqu'à ce qu'il trouve le nom de la propriété 'Type'.
public override IBaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
Utf8JsonReader typeReader = reader;
if (typeReader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
while (typeReader.Read())
{
if (typeReader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
string propertyName = typeReader.GetString();
if (propertyName.Equals(nameof(IBaseClass.Type)))
{
break;
}
typeReader.Skip();
}
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
IGraphOptions baseClass = default;
GraphType type = (GraphType)typeReader.GetInt32();
....
// The switch..
....
Pour être honnête, je pense que la façon dont ce JsonConverter System.Text personnalisé est configuré est inutilement complexe et je préfère le JsonConverter de Newtonsoft.
- Réponses précédentes
- Plus de réponses