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.
    
  
  Anzeige
  
    
  
    
    
  
  
  
  
    
      
  
    
      
        
  
    
  
  
        
      
    
    
      
      
        
          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}.";
 }
}
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":
  
  
    
      Anzeige
    
    
  
{"$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)