1. Introduction
Imagine: you wrote a class Person, serialized an object to a file, and a couple months later decided that it needs, say, a new residence address or you changed types of some properties. Sounds routine — but when you try to load (deserialize) previously saved data in the old format, surprises may await: something won't load, an exception might be thrown, some values will be empty or even wrong.
Such behavior is a typical case of breaking backward compatibility. In real development this happens more often than students forget to put a semicolon (i.e., very often).
Let's illustrate the problem with an example
Consider our small teaching project. Suppose at this stage we had this class:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Serialize an instance of that class to JSON:
Person p = new Person { Name = "Alice", Age = 35 };
string json = JsonSerializer.Serialize(p);
File.WriteAllText("person.json", json);
We got in the file:
{"Name":"Alice","Age":35}
Now, a week later we decide to make our app fancier and add an address field:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; } // new field
}
And then — try loading the old file:
string json = File.ReadAllText("person.json");
Person p = JsonSerializer.Deserialize<Person>(json);
What will happen? The address won't be present in our object: the Address property will be null. No error occurred. For now it works... But once you start changing types, removing fields or doing something really "interesting", problems may start!
2. What kinds of changes exist — and how they affect things
Changes in class structure affect serialization in different ways. Let's go through several typical scenarios.
Adding new properties
This is the least dangerous case. Old data (where those properties didn't exist) deserializes fine: new properties will get default values (null for reference types, 0 for int, etc.).
Note: If your new property is non-nullable and doesn't have a "reasonable" default, you may run into trouble (especially with C# 11+ required properties).
Removing properties
If you remove a property but it still exists in serialized data — the serializer will most likely just ignore the "extra" data and loading will still succeed.
But this depends on the serializer. For example, JsonSerializer and Newtonsoft.Json are pretty lenient: they won't throw an exception, while some old or custom serializers might behave differently.
Renaming properties
Here is where the fun begins. If you simply rename property FirstName to Name, the serializer can't map fields from old data to the new object. In other words, the field will be empty (null/0), and the old one in the file will be ignored.
Changing a property's type
For example, you had public int Age, then decided to make it public string Age (maybe someone will write "bessmertny" — anything can happen). Trying to deserialize old data may lead to an error ("Cannot convert number to string") or the property will just get the default value. It depends on the serializer and its strict typing settings.
Changing hierarchies (inheritance, nesting)
If you change base classes, move properties around, or make one class a wrapper for another — old serialized data may become completely incompatible. This is especially true for XML and complex object hierarchies.
3. Compatibility issues
How to detect compatibility problems?
Often a compatibility issue doesn't show up immediately or clearly: your app just starts behaving "weird", some data gets lost, or a vague exception appears in logs. Most issues surface when:
- A user loads an old file into a new version of the program.
- The server receives JSON/XML from a "legacy" client.
- You're working with an external API that suddenly updated its interface.
Symptoms vary: from deserialization errors to "unexpectedly" empty fields.
Serializer impact on compatibility
Not all serializers behave the same. JSON serializers tend to be the most tolerant to structural changes — both the standard System.Text.Json and Newtonsoft.Json are good examples. They typically skip unknown properties from the file and don't serialize unknown object fields back.
With XML it's a bit stricter: if the root element or hierarchy changes, errors may occur.
In binary formats you can even get exceptions if order or types changed!
4. How to minimize risks? Approaches and practices
Here are some approaches that help minimize (or sometimes completely avoid) unpleasant surprises.
Use versions for classes and data
Add a special Version field to serializable objects or to files. This lets you determine which version of the structure created the file and take appropriate action on load (for example, run a data upgrade).
public class PersonV2
{
public int Version { get; set; } = 2;
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
Use name-mapping attributes (for serialization)
With JSON and XML you can explicitly specify how a property should be named in serialized form. If you rename a property — keep the old name:
public class Person
{
[JsonPropertyName("FirstName")] // for System.Text.Json
[JsonProperty("FirstName")] // for Newtonsoft.Json
public string Name { get; set; }
public int Age { get; set; }
}
Use nullable types and default values
If you add new fields that might not be present in old data — make them nullable or assign a default value so deserialization doesn't break:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string? Address { get; set; } = "Unknown";
}
Handle "unknown field" events
With Newtonsoft.Json you can subscribe to handling "mystery" fields via a special setting, to, for example, log potentially dangerous situations.
var settings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error
};
try
{
var person = JsonConvert.DeserializeObject<Person>(json, settings);
}
catch (JsonSerializationException ex)
{
Console.WriteLine("Failed to deserialize: " + ex.Message);
}
Data migration
If changes are significant, it's smarter to plan a migration step: load data into the "old" structure, then transform it into the new one:
// Suppose PersonV1 had no address
public class PersonV1 { public string Name; public int Age; }
// New one — with 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. Complex cases and unexpected errors
Field invariants and required properties
C# 11+ introduced required properties. Now if a field is marked required, deserialization can throw an error if that field is missing in the data:
public class Person
{
public string Name { get; set; }
[JsonPropertyName("Age")]
public required int Age { get; set; }
public string Address { get; set; }
}
If Age is missing in old data — you'll get a structural mismatch exception.
Type change: int → string
// Before:
public class Record { public int Count; }
// After:
public class Record { public string Count; }
If the data contains "Count":42, deserializing into string might work (smart-conversion), but the reverse direction may throw.
Removing a base class
If a serialized object used inheritance and you changed the hierarchy — deserializing old files can fail, sometimes "silently", sometimes loudly.
6. Common mistakes when working on compatibility
Mistake #1: mindlessly changing existing properties.
Renaming or changing property types without accounting for existing serialized data leads to data loss on deserialization.
Mistake #2: forgetting nullable types for new fields.
New properties should be either nullable or have reasonable default values.
Mistake #3: not testing backward compatibility.
Changed a class — make sure to verify that old files/data still load correctly.
Mistake #4: mixing attributes from different libraries.
Don't use JsonPropertyName and JsonProperty on the same property simultaneously.
GO TO FULL VERSION