1. Einführung
In dieser Lektion machen wir den Schritt von "gib mir einfach die Liste zu speichern" zu flexibler, genauer und performanter Serialisierung mit System.Text.Json. In modernen .NET‑Projekten ist JSON de‑facto der Standard zum Datenaustausch. Die grundlegenden Aufrufe Serialize/Deserialize sind einfach, aber echte Aufgaben erfordern feine Einstellungen: Felder ignorieren/umbenennen, Datumsformate steuern, Schutz vor Zyklen, Umgang mit großen Datenmengen, eigene Converter usw.
Wir schauen uns nicht nur die Methoden des JsonSerializer an, sondern auch die Parameter über JsonSerializerOptions, Attribute, Arbeiten mit Stream, Speicherverwaltung und wie man eigene Serialisierungsregeln über JsonConverter einführt.
Kurze Historie und Positionierung von System.Text.Json
Lange Zeit dominierte in .NET Newtonsoft.Json (Json.NET) — flexibel und ausgereift, aber nicht immer am schnellsten oder leichtgewichtigsten in Sachen Dependencies. Seit .NET Core 3.0 gibt es das eingebaute System.Text.Json: hohe Performance, minimale Abhängigkeiten (Teil der Plattform), enge Integration mit ASP.NET Core und stetige Weiterentwicklung mit den .NET‑Releases.
2. Grundklassen und Methoden
Das zentrale Element ist die statische Klasse JsonSerializer, die zwei Richtungen anbietet:
- Serialisierung: Objekt → JSON‑String (Serialize)
- Deserialisierung: JSON‑String → Objekt des gewünschten Typs (Deserialize)
Beispiel: Serialisierung eines einfachen Objekts
using System.Text.Json;
var person = new Person { Name = "Ivan", Age = 30 };
string jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString); // {"Name":"Ivan","Age":30}
Beispiel: Deserialisierung
var json = "{\"Name\":\"Anna\",\"Age\":22}";
var anna = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(anna.Name); // Anna
Hinweis: der Typ Person wurde in früheren Vorlesungen bereits definiert — wir verwenden ihn auch hier.
3. Steuerung der Serialisierung: JsonSerializerOptions
In realen Projekten braucht man fast immer Einstellungen: Eigenschaftsnamen in camelCase, Datumsformate, Zyklusbehandlung, Default‑Werte usw. All das wird über JsonSerializerOptions gesteuert.
Beispiel für Einstellungen
var options = new JsonSerializerOptions
{
WriteIndented = true, // JSON schön formatieren (fügt Leerzeichen und Zeilenumbrüche hinzu)
PropertyNameCaseInsensitive = true, // Groß-/Kleinschreibung der Property‑Namen beim Deserialisieren ignorieren
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // camelCase für Properties (statt PascalCase)
};
string json = JsonSerializer.Serialize(person, options);
/*
{
"name": "Ivan",
"age": 30
}
*/
Warum das wichtig ist? Die meisten Frontend‑Frameworks erwarten genau camelCase und nicht das .NET‑typische PascalCase.
4. Attribute: System.Text.Json.Serialization
Manchmal ist es bequemer, die Serialisierung direkt im Modell über Attribute zu steuern. Die werden an Felder/Properties angehängt, um Namen, Ein-/Ausschluss und Verhalten zu beeinflussen.
Wichtige Attribute
| Attribut | Was es macht |
|---|---|
|
Schließt die Property von Serialisierung/Deserialisierung aus |
|
Verwendet einen anderen Namen im JSON |
|
Schließt nicht‑öffentliche Property/Feld in die Serialisierung ein |
|
Steuert die Behandlung numerischer Werte |
Beispiel: Steuerung von Properties via Attribute
using System.Text.Json.Serialization;
public class Person
{
[JsonPropertyName("full_name")]
public string Name { get; set; }
[JsonIgnore]
public int SecretCode { get; set; }
public int Age { get; set; }
}
var person = new Person { Name = "Pjotr", Age = 45, SecretCode = 123 };
string json = JsonSerializer.Serialize(person);
// {"full_name":"Pjotr","Age":45}
Beachte: SecretCode ist nicht im JSON, und Name wurde als "full_name" serialisiert.
5. Serialisierung von Collections und verschachtelten Objekten
Collections — das ist einfach
var numbers = new List<int> { 1, 2, 3 };
string json = JsonSerializer.Serialize(numbers); // [1,2,3]
var people = new List<Person> {
new Person { Name = "Anna", Age = 20 },
new Person { Name = "Maxim", Age = 40 }
};
string jsonList = JsonSerializer.Serialize(people);
// [{"Name":"Anna","Age":20},{"Name":"Maxim","Age":40}]
Verschachtelte Strukturen
public class Group
{
public string Name { get; set; }
public List<Person> Members { get; set; }
}
var group = new Group
{
Name = "Entwickler",
Members = new List<Person>
{
new Person { Name = "Sasha", Age = 23 },
new Person { Name = "Masha", Age = 28 }
}
};
string jsonGroup = JsonSerializer.Serialize(group, options);
/*
{
"name": "Entwickler",
"members": [
{ "name": "Sasha", "age": 23 },
{ "name": "Masha", "age": 28 }
]
}
*/
6. Deserialisierung: was wichtig ist
var json = "[{\"Name\":\"Ivan\",\"Age\":21}]";
var list = JsonSerializer.Deserialize<List<Person>>(json);
Console.WriteLine(list[0].Name); // Ivan
Ein typisches Szenario: fehlt ein Feld im JSON, bekommt die entsprechende Property den Default‑Wert. Zusätzliche Felder im JSON, die nicht im Modell vorhanden sind, werden ignoriert. Wenn jedoch die Typen nicht übereinstimmen (z.B. statt einer Zahl kommt ein String), wird beim Deserialisieren eine Ausnahme geworfen.
7. Umgang mit Datums-/Zeit-, Format- und Zahlenwerten
public class Meeting
{
public string Topic { get; set; }
public DateTime Time { get; set; }
}
var meeting = new Meeting { Topic = "Besprechung", Time = DateTime.Now };
string json = JsonSerializer.Serialize(meeting);
// {"Topic":"Besprechung","Time":"2024-06-06T20:30:00.0000000+03:00"}
Standardmäßig wird DateTime als ISO 8601 serialisiert. Brauchst du ein anderes Format (z.B. nur das Datum)? Verwende eine separate Property oder einen Custom Converter (siehe unten).
FAQ: Damit Zahlen als Strings serialisiert werden (z.B. Telefonnummern oder große IDs), benutze das Attribut [JsonNumberHandling(JsonNumberHandling.WriteAsString)].
8. Streams und Arbeiten mit Dateien
Man kann nicht nur mit Strings arbeiten, sondern auch mit Stream — wichtig bei großen Datenmengen (Dateien, Netzwerk).
Beispiel: in eine Datei schreiben
using var fs = File.Create("person.json");
JsonSerializer.Serialize(fs, person);
// Vergiss nicht fs.Flush() aufzurufen oder using zu verwenden!
Beispiel: aus einer Datei lesen
using var fs = File.OpenRead("person.json");
var restored = JsonSerializer.Deserialize<Person>(fs);
Für Streams gibt es asynchrone Methoden SerializeAsync/DeserializeAsync — nützlich für stark ausgelastete Services.
9. Custom Converter
Wenn die Standardregeln nicht passen (nicht‑standardisierte Datums-/Zahlenformate, komplexe Werte, eigene Strukturen) — dann schreibt man einen JsonConverter.
Beispiel: Datum nur als "dd.MM.yyyy"
public class CustomDateConverter : JsonConverter<DateTime>
{
public override DateTime Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.ParseExact(reader.GetString(), "dd.MM.yyyy", null);
}
public override void Write(
Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("dd.MM.yyyy"));
}
}
var options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateConverter());
var dt = new DateTime(2024, 6, 1);
string json = JsonSerializer.Serialize(dt, options); // "01.06.2024"
Custom Converter sind nützlich für Serialisierung von Koordinaten, Vektoren, Farben, nicht‑standardmäßigen Datums- und Währungsformaten, verschiedenen ID‑Formaten usw.
10. Nützliche Feinheiten
Umgang mit zyklischen Referenzen und tiefen Hierarchien
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve, // Erhält alle Objekte mit $id/$ref
WriteIndented = true
};
Wichtig: im JSON erscheinen dann die internen Properties $id und $ref. Für den Austausch mit externen Systemen, die das nicht verstehen, kann das ungeeignet sein.
Unterschiede zwischen System.Text.Json und Newtonsoft.Json
System.Text.Json ist schon sehr mächtig, deckt aber noch nicht alle Szenarien von Newtonsoft.Json ab (private Konstruktoren, komplexe dynamische Objekte, etc.). Für die meisten Standardaufgaben empfehlen wir den eingebauten Serializer — er ist schneller und vermeidet unnötige Abhängigkeiten.
Interaktives Arbeiten mit JSON: das DOM-API
Wenn du durch JSON „iterieren“ willst, ohne ein komplettes Modell zu definieren, nutze JsonDocument und JsonElement.
using var doc = JsonDocument.Parse(jsonString);
JsonElement root = doc.RootElement;
if (root.TryGetProperty("Name", out var nameProperty))
{
Console.WriteLine(nameProperty.GetString());
}
11. Nützliche Optionen und ihre Effekte
| Eigenschaft | Wert/Zweck |
|---|---|
|
true — formatieren mit Einrückungen |
|
true — Groß-/Kleinschreibung der Property‑Namen beim Deserialisieren ignorieren |
|
|
|
Regeln zum Ignorieren von null/Default‑Werten |
|
, |
|
true — erlauben eines Kommas am Ende eines Arrays |
|
Konvertierung von Zahlen zu Strings/zurück (und mehr) |
|
Liste von Custom Convertern |
12. Häufige Fehler und praktische Tipps
Fehler Nr.1: falscher Typ bei der Deserialisierung. Wenn du eine Liste serialisiert hast, deserialisiere auch in eine Liste: List<T>, nicht in ein einzelnes Objekt.
Fehler Nr.2: falsche Groß-/Kleinschreibung von Property‑Namen. Ohne richtige Einstellungen können Properties „nicht gefunden“ werden. Verwende PropertyNameCaseInsensitive oder setze eine passende PropertyNamingPolicy.
Fehler Nr.3: fehlerhafte Datumsbehandlung. Standard ist ISO 8601. Brauchst du ein anderes Format — schreib und nutze einen Converter (JsonConverter<DateTime>).
Fehler Nr.4: Erwartung, dass private/static Felder serialisiert werden. Standardmäßig werden public Properties verwendet. Für Sonderfälle nutze passende Attribute (z.B. [JsonInclude]).
Fehler Nr.5: Missverständnis von Default‑Werten. Fehlende Felder im JSON → Default‑Wert für die Property. Berücksichtige das in deiner Logik.
Fehler Nr.6: falsches Management von Streams. Schließe Ressourcen mit using oder await using, um Leaks und Deadlocks zu vermeiden.
GO TO FULL VERSION