Sie sind hier: Weblog

Neu in .NET 7.0 [24]: Polymorphismus beim Serialisieren mit System.Text.Json

Foto Dr. Holger Schwichtenberg, Dr. Holger Schwichtenberg
28.07.2023 10:22:00

Inhaltsverzeichnis

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.

Der Dotnet-Doktor – Holger Schwichtenberg

Dr. Holger Schwichtenberg ist Chief Technology Expert bei MAXIMAGO, die Innovations- und Experience-getriebener Softwareentwicklung, u.a. in hochkritischen sicherheitstechnischen Bereichen, anbietet. Zudem ist er Leiter des Expertennetzwerks www.IT-Visions.de, das mit 38 renommierten Experten zahlreiche mittlere und große Unternehmen durch Beratung und Schulung bei der Entwicklung sowie dem Betrieb von Software unterstützt.

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}.";
 }
}

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"}

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).

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.

Im nächsten Teil dieser Serie, der in der kommenden Woche erscheinen wird, geht es um Polymorphismus beim JSON-Deserialisieren.

(map)