In der JSON-Bibliothek System.Text.Json
gibt es seit Version 7.0 die Annotation [JsonDerivedType]
. Damit kann man bei einer Basisklasse sogenannte Typ-Diskriminatoren für die Basisklasse und die abgeleiteten Klassen deklarieren. Diese werden bei der Serialisierung und Deserialisierung berücksichtigt.
Dr. Holger Schwichtenberg ist technischer Leiter des Expertennetzwerks www.IT-Visions.de, das mit 53 renommierten Experten zahlreiche mittlere und große Unternehmen durch Beratungen und Schulungen sowie bei der Softwareentwicklung unterstützt. Durch seine Auftritte auf zahlreichen nationalen und internationalen Fachkonferenzen sowie mehr als 90 Fachbücher und mehr als 1500 Fachartikel gehört Holger Schwichtenberg zu den bekanntesten Experten für .NET und Webtechniken in Deutschland.
Beispiel: Gegeben sei eine Basisklasse Person
und eine abgeleitete Klasse Consultant
.
[JsonDerivedType(typeof(Person), typeDiscriminator: "P")]
[JsonDerivedType(typeof(Consultant), typeDiscriminator: "C")]
public class Person
{
public required int ID { get; set; }
public required string Name { get; set; }
public override string ToString()
{
return $"Person {Name}";
}
}
public class Consultant : Person
{
public string? Company { get; set; }
public override string ToString()
{
return $"Consultant {Name} arbeitet bei {Company}.";
}
}
Serialisierung erzeugt die Zusatzeigenschaft $type
Wenn man nun eine Instanz von Person erzeugt und diese in JSON serialisiert
Person p = new Person() { ID = 123, Name = "Holger Schwichtenberg" };
var json1 = JsonSerializer.Serialize(p);
erhält man diese JSON-Zeichenkette mit dem Zusatz "$type":"P":
{"$type":"P","ID":123,"Name":"Holger Schwichtenberg"}
Ohne die Angabe von [JsonDerivedType]
hätte man bekommen:
{"ID":123,"Name":"Holger Schwichtenberg"}
Ebenso erhält man hier ein "C", wenn man ein Consultant-Objekt serialisiert, selbst wenn die Variable vom Basistyp der Basisklasse Person ist, d. h.
Person c = new Consultant() { ID = 123, Name = "Holger Schwichtenberg", Company = "www.IT-Visions.de" };
var json2 = JsonSerializer.Serialize(c);
liefert
{"$type":"C","Company":"www.IT-Visions.de","ID":123,"Name":"Holger Schwichtenberg"}
Ohne die Angabe von [JsonDerivedType]
hätte man wieder nur das bekommen:
{"ID":123,"Name":"Holger Schwichtenberg"}
Würde man bei der Deklarierung ohne einen Typ-Diskriminator die Variable c
auf Consultant
statt auf Person
typisieren
Consultant c = new Consultant() { ID = 123, Name = "Holger Schwichtenberg", Company = "www.IT-Visions.de" };
var json2 = JsonSerializer.Serialize(c);
dann wäre das Ergebnis
{"Company":"www.IT-Visions.de","ID":123,"Name":"Holger Schwichtenberg"}
Das bedeutet: [JsonDerivedType]
dient nicht nur dazu, die Zusatzangabe $type in der JSON-Zeichenkette zu bekommen, sondern auch die zusätzlichen Eigenschaften eines abgeleiteten Typs zu serialisieren, wenn im Code nicht der konkrete Typ, sondern eine Basisklasse verwendet wird. [JsonDerivedType]
unterstützt also polymorphes Programmieren.
Wichtig: Wenn es noch eine weitere abgeleitete Klasse Developer
gibt, für die aber keine Annotation [JsonDerivedType]
in der Basisklasse existiert
public class Developer : Person
{
public string? Company { get; set; }
public override string ToString()
{
return $"Developer {Name} entwickelt bei {Company}";
}
}
dann gibt es einen Laufzeitfehler
Runtime type 'Developer' is not supported by polymorphic type 'Person'
,
wenn man das versucht:
Person d = new Developer() { ID = 123, Name = "Holger Schwichtenberg", Company = "MAXIMAGO GmbH" };
var json3 = JsonSerializer.Serialize(d);
Dieses Verhalten kann man ändern. Mit einem zusätzlichen
[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
Auf der Basisklasse erreicht man, dass immer die Personen-Property des Developer-Objekts serialisiert werden, wenn es keinen passenden Typ-Diskriminator gibt:
{"$type":"P","ID":123,"Name":"Holger Schwichtenberg"}
Weitere Optionen
Der typeDiscriminator
kann anstelle einer Zeichenkette auch eine Zahl sein:
[JsonDerivedType(typeof(Person), typeDiscriminator: 0)]
[JsonDerivedType(typeof(Consultant), typeDiscriminator: 1)]
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"Person {Name}";
}
}
Statt $type
kann man bei der Serialisierung und Deserialisierung einen anderen Namen verwenden, indem man dies mit der Annotation [JsonPolymorphic]
auf der Basisklasse deklariert:
[JsonDerivedType(typeof(Person), typeDiscriminator: "P")]
[JsonDerivedType(typeof(Consultant), typeDiscriminator: "C")]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$class")] // Standard ist $type
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"Person {Name}";
}
}
Auch in den in Teil 22 behandelten Type Info Resolvers kann man das polymorphe Verhalten via typeInfo.PolymorphismOptions
konfigurieren (siehe dazu den Microsoft-Blogeintrag zu System.Text.Json).
Verfügbarkeit
System.Text.Json
ist zusammen mit .NET 7.0 als NuGet-Paket erschienen, läuft aber auch unter .NET Standard 2.0 und damit auch auf .NET Core 2.x/3.x sowie .NET 5.0/.NET 6.0 auf dem klassischen .NET Framework ab Version 4.6.2.
Ausblick
Im nächsten Teil dieser Serie, der in der kommenden Woche erscheinen wird, geht es um Polymorphismus beim JSON-Deserialisieren.
(map)