1. Einführung
Stell dir vor: du hast eine Klasse Person geschrieben, ein Objekt in eine Datei serialisiert, und ein paar Monate später entschieden, dass es zum Beispiel eine neue Wohnadresse braucht oder du den Typ einiger Eigenschaften geändert hast. Klingt normal — aber wenn du versuchst, zuvor gespeicherte Daten im alten Format zu laden (deserialisieren), kann es Überraschungen geben: etwas lädt nicht, eine Ausnahme wird geworfen, einige Werte sind leer oder sogar falsch.
So ein Verhalten ist ein typischer Fall von gebrochener Rückwärtskompatibilität. In echter Entwicklung passiert das öfter, als Studierende das Semikolon vergessen (also sehr oft).
Stellen wir das Problem am Beispiel dar
Betrachten wir unser kleines Lernprojekt. Angenommen, zu diesem Zeitpunkt hatten wir diese Klasse:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Wir serialisieren eine Instanz dieser Klasse in JSON:
Person p = new Person { Name = "Alice", Age = 35 };
string json = JsonSerializer.Serialize(p);
File.WriteAllText("person.json", json);
In der Datei haben wir:
{"Name":"Alice","Age":35}
Eine Woche später entscheiden wir, unsere App moderner zu machen und ein Adressfeld hinzuzufügen:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; } // neues Feld
}
Und dann versuchen wir, die alte Datei zu laden:
string json = File.ReadAllText("person.json");
Person p = JsonSerializer.Deserialize<Person>(json);
Was passiert? Die Address wird in unserem Objekt fehlen: die Eigenschaft Address ist null. Es tritt kein Fehler auf. Bis hierhin funktioniert alles... Aber sobald du anfängst, Typen zu ändern, Felder zu löschen oder etwas wirklich "Interessantes" zu tun, können Probleme beginnen!
2. Welche Arten von Änderungen gibt es — und wie wirken sie sich aus
Änderungen an der Klassenstruktur beeinflussen die Serialisierung unterschiedlich. Schauen wir uns einige typische Szenarien an.
Hinzufügen neuer Eigenschaften
Das ist die ungefährlichste Variante. Alte Daten (in denen diese Eigenschaften nicht existierten) werden normalerweise problemlos deserialisiert: neue Eigenschaften erhalten Default-Werte (null für Referenztypen, 0 für int usw.).
Achtung: Wenn deine neue Eigenschaft nicht nullable ist und keinen "sinnvollen" Default-Wert hat, kann es Probleme geben (besonders bei required-Eigenschaften ab C# 11+).
Löschen von Eigenschaften
Wenn du eine Eigenschaft entfernst, die noch in serialisierten Daten vorhanden ist, wird der Serializer die "überflüssigen" Daten höchstwahrscheinlich einfach ignorieren und das Laden wird trotzdem funktionieren.
Das hängt aber vom verwendeten Serializer ab. Zum Beispiel sind JsonSerializer und Newtonsoft.Json ziemlich tolerant: sie werfen normalerweise keine Ausnahme, während einige alte oder custom Serializer sich anders verhalten können.
Umbenennen von Eigenschaften
Hier fängt der Spaß an. Wenn du eine Eigenschaft einfach von FirstName in Name umbenennst, kann der Serializer keine Zuordnung zwischen den Feldern der alten Daten und dem neuen Objekt herstellen. Anders gesagt: das Feld bleibt leer (null/0), und das alte Feld aus der Datei wird ignoriert.
Änderung des Typs einer Eigenschaft
Zum Beispiel hattest du früher public int Age, dann entscheidest du, es in public string Age zu ändern (vielleicht schreibt jemand "unsterblich"). Der Versuch, alte Daten zu deserialisieren, kann zu einem Fehler führen ("Cannot convert number to string") oder die Eigenschaft bekommt einfach den Default-Wert. Das hängt vom konkreten Serializer und seinen strikten Typisierungs-Einstellungen ab.
Änderungen in Hierarchien (Vererbung, Verschachtelung)
Wenn du Basisklassen änderst, Eigenschaften verschiebst oder etwa eine Wrapper-Klasse einführst — alte serialisierte Daten können komplett inkompatibel werden. Besonders betroffen sind XML und komplexe Objekt-Hierarchien.
3. Kompatibilitätsprobleme
Wie erkennt man Kompatibilitätsprobleme?
Oft zeigt sich ein Kompatibilitätsfehler nicht sofort und nicht offensichtlich: deine App verhält sich einfach "merkwürdig", ein Teil der Daten geht verloren oder in den Logs erscheint eine wenig aussagekräftige Exception. Meistens treten Probleme auf, wenn:
- Ein Benutzer eine alte Datei in eine neue Version der Anwendung lädt.
- Ein Server JSON/XML von einem "alten" Client erhält.
- Du mit einem externen API arbeitest, dessen Schnittstelle plötzlich aktualisiert wurde.
Die Symptome reichen von Deserialisierungsfehlern bis zu "unerwartet" leeren Feldern.
Einfluss des Serializers auf die Kompatibilität
Nicht alle Serializer verhalten sich gleich. JSON-Serializer sind in der Regel am tolerantesten gegenüber Strukturänderungen — sowohl der Standard System.Text.Json als auch Newtonsoft.Json. Sie lassen normalerweise unbekannte Properties aus der Datei unbeachtet und serialisieren unbekannte Felder des Objekts nicht zurück.
Bei XML ist das etwas strenger: wenn sich das Root-Element oder die Hierarchie ändert, können Fehler auftreten.
Bei binären Formaten sind Exceptions möglich, wenn Reihenfolge oder Typen geändert wurden!
4. Wie minimiert man Risiken? Ansätze und Praktiken
Hier ein paar Ansätze, die helfen, mögliche Probleme zu minimieren (und manchmal ganz zu vermeiden).
Verwendet Versionen für Klassen und Daten
Fügt den serialisierbaren Objekten oder den Dateien ein spezielles Feld Version hinzu. So kann man erkennen, mit welcher Strukturversion die Datei erstellt wurde, und beim Laden die passende Entscheidung treffen (z. B. ein Upgrade der Daten durchführen).
public class PersonV2
{
public int Version { get; set; } = 2;
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
Verwendet Attribute zur Namenszuordnung (für die Serialisierung)
In JSON und XML kannst du explizit angeben, wie eine Eigenschaft in der serialisierten Form heißen soll. Wenn du eine Eigenschaft umbenennst — behalte den alten Namen bei:
public class Person
{
[JsonPropertyName("FirstName")] // für System.Text.Json
[JsonProperty("FirstName")] // für Newtonsoft.Json
public string Name { get; set; }
public int Age { get; set; }
}
Nutze nullable-Typen und Default-Werte
Wenn neue Felder auftauchen, die in alten Daten nicht immer vorhanden sind — mache sie nullable oder setze einen Default-Wert, damit die Deserialisierung keine Probleme verursacht:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string? Address { get; set; } = "Unknown";
}
Behandlung von "unbekannten Feld"-Ereignissen
In Newtonsoft.Json kann man auf unbekannte Felder mit einem speziellen Handler reagieren, z. B. um potenzielle Probleme zu loggen.
var settings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error
};
try
{
var person = JsonConvert.DeserializeObject<Person>(json, settings);
}
catch (JsonSerializationException ex)
{
Console.WriteLine("Konnte nicht deserialisieren: " + ex.Message);
}
Datenmigration
Wenn Änderungen erheblich sind, ist es klüger, eine Migrationsstufe vorzusehen: z. B. die Daten in die "alte" Struktur laden und dann in die neue umwandeln:
// Angenommen, Klasse PersonV1 hatte kein address
public class PersonV1 { public string Name; public int Age; }
// Neu — mit address
public class PersonV2 { public string Name; public int Age; public string Address; }
// Migration:
string oldJson = File.ReadAllText("person.json");
PersonV1 oldPerson = JsonSerializer.Deserialize<PersonV1>(oldJson);
PersonV2 migrated = new PersonV2
{
Name = oldPerson.Name,
Age = oldPerson.Age,
Address = "Unknown"
};
5. Komplexe Fälle und unerwartete Fehler
Feldinvarianz und required-Eigenschaften
Ab C# 11 gibt es required-Eigenschaften. Wenn ein Feld als required markiert ist, kann die Deserialisierung fehlschlagen, falls dieses Feld in den Daten fehlt:
public class Person
{
public string Name { get; set; }
[JsonPropertyName("Age")]
public required int Age { get; set; }
public string Address { get; set; }
}
Wenn in alten Daten das Feld Age fehlt — wird eine Ausnahme über die Struktur-Abweichung geworfen.
Typänderung: int → string
// War:
public class Record { public int Count; }
// Ist jetzt:
public class Record { public string Count; }
Wenn in den Daten "Count":42 steht, kann die Deserialisierung in einen string funktionieren (Smart-Konvertierung), aber in die andere Richtung kann eine Ausnahme auftreten.
Löschen der Basisklasse
Wenn ein serialisiertes Objekt vererbt war und die Hierarchie geändert wurde — kann die Deserialisierung alter Dateien zu Fehlern führen, manchmal "still", manchmal deutlich.
6. Typische Fehler bei der Arbeit an Kompatibilität
Fehler Nr.1: gedankenloses Ändern bestehender Eigenschaften.
Umbenennen oder Typwechsel von Eigenschaften ohne Berücksichtigung vorhandener serialisierter Daten führt zum Informationsverlust bei der Deserialisierung.
Fehler Nr.2: nullable-Typen für neue Felder vergessen.
Neue Eigenschaften sollten entweder nullable sein oder sinnvolle Default-Werte haben.
Fehler Nr.3: Rückwärtskompatibilität nicht testen.
Klasse geändert — unbedingt prüfen, dass alte Dateien/Daten noch korrekt geladen werden.
Fehler Nr.4: Attribute verschiedener Bibliotheken mischen.
Man sollte nicht JsonPropertyName und JsonProperty gleichzeitig auf derselben Eigenschaft verwenden.
GO TO FULL VERSION