1. 介紹
想像一下:你寫了一個類別 Person,把物件序列化到檔案,幾個月後你決定要加個地址欄位或改變某些屬性的型別。看起來是很平常的事 —— 但當你試圖讀(反序列化)以前存的舊格式資料時,可能會有驚喜:有東西讀不出來、丟出例外、某些值變空或甚至不正確。
這種行為就是典型的向下相容性破壞。在真實開發中這事比學生忘記分號還常發生(也就是非常常)。
用範例來說明問題
看我們的教學小專案。假設目前我們有這樣一個類別:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
把這個類別的實例序列化成 JSON:
Person p = new Person { Name = "Alice", Age = 35 };
string json = JsonSerializer.Serialize(p);
File.WriteAllText("person.json", json);
檔案會得到:
{"Name":"Alice","Age":35}
現在過了一週,我們想讓 App 更時髦,新增一個地址欄位:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; } // 新增欄位
}
然後嘗試載入舊檔案:
string json = File.ReadAllText("person.json");
Person p = JsonSerializer.Deserialize<Person>(json);
會發生什麼?我們的物件裡不會有 Address:Address 會是 null。沒有例外,一切看起來還能運作... 但只要你開始改型別、刪欄位或做更「有趣」的改動,就可能出問題!
2. 常見的變更類型 — 以及它們的影響
類別結構的變更會以不同方式影響序列化。下面拆幾個常見情境來討論。
新增屬性
這是最不危險的一種情況。舊資料(沒有這些欄位)通常可以被正常反序列化:新屬性會等於預設值(參考型別為 null,0 對 int 等等)。
注意: 如果你的新屬性是 non-nullable 且沒有合理的預設值,可能會出問題(特別是遇到 C# 11+ 的 required 屬性時)。
刪除屬性
如果你刪掉了屬性,但序列化資料裡還有該欄位——序列化器通常會忽略這些「多餘」的欄位,載入仍會成功。
但這也取決於你用的序列化庫。例如,JsonSerializer 與 Newtonsoft.Json 都相當寬容:它們不會丟例外;不過某些老舊或自訂的序列化器可能行為不同。
重命名屬性
這時就有趣了。如果你把 FirstName 改成 Name,序列化器無法把舊資料的欄位對應到新物件。換句話說,那個欄位會是空(null/0),舊檔案裡的值被忽略。
變更屬性型別
比方說原本你有 public int Age,後來改成 public string Age(也許有人會寫 "長生不老" —— 任何事都有可能)。嘗試把舊資料反序列化會依序列化器與它的型別強型設定,結果可能是發生錯誤(例如 "Cannot convert number to string")或屬性拿到預設值。
改變繼承或結構層級(繼承、嵌套)
如果你調整了基底類別、把屬性移到別處或把一個類包成另一個類包裝——舊的序列化資料可能會完全不相容。尤其是針對 XML 與複雜物件繼承結構時,問題更容易出現。
3. 相容性問題
如何發現相容性問題?
相容性問題常常不會立刻或明顯地爆出:你的應用可能開始「怪怪的」,部分資料消失,或 log 裡出現不太友善的例外。通常問題會在以下情形浮現:
- 使用者在新版本程式中載入舊檔案。
- 伺服器從「舊版本」的客戶端收到 JSON/XML。
- 你對外部 API 的介面被不預期地更新了。
症狀很多樣:從反序列化錯誤到某些欄位「莫名其妙」是空的。
序列化器對相容性的影響
不同序列化器行為不一。對 JSON 結構變動最「寬容」的通常是像標準的 System.Text.Json 或 Newtonsoft.Json。它們通常會略過檔案中不認識的屬性,而且不會把未知的物件欄位序列化回去。
在 XML 上就稍微嚴格一點:如果根元素或結構改變,可能會出現錯誤。
而在二進位格式裡,如果順序或型別改了,甚至可能直接丟例外!
4. 怎麼降低風險?方法與實務
下面幾個做法可以把問題風險降到最低(有時能完全避免)。
使用類別與資料的版本號
在可序列化物件或檔案中加入一個 Version 欄位。這能讓你知道檔案是用哪個結構版本建立,載入時就能依版本做不同處理(例如跑資料升級)。
public class PersonV2
{
public int Version { get; set; } = 2;
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
對序列化使用名稱映射屬性
對 JSON 或 XML 可以明確指定序列化時屬性的名稱。重命名屬性時保留舊名稱可以避免中斷:
public class Person
{
[JsonPropertyName("FirstName")] // 適用於 System.Text.Json
[JsonProperty("FirstName")] // 適用於 Newtonsoft.Json
public string Name { get; set; }
public int Age { get; set; }
}
使用 nullable 型別與預設值
如果你新增的欄位在舊資料中可能不存在——把它設成 nullable 或指定合理預設值,這樣反序列化才不會出問題:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string? Address { get; set; } = "Unknown";
}
處理「未知欄位」事件
在 Newtonsoft.Json 裡可以訂閱未知欄位的處理函式,比方說記 log 或做其他自訂處理,避免默默地丟失資訊。
var settings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error
};
try
{
var person = JsonConvert.DeserializeObject<Person>(json, settings);
}
catch (JsonSerializationException ex)
{
Console.WriteLine("無法反序列化: " + ex.Message);
}
資料遷移
若改動很大,最好設計遷移步驟:先把資料載入「舊」結構,再轉換成新結構:
// 假設 PersonV1 類沒有 Address
public class PersonV1 { public string Name; public int Age; }
// 新的有 Address
public class PersonV2 { public string Name; public int Age; public string Address; }
// 遷移:
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. 複雜狀況與意外錯誤
欄位不變性與 required 屬性
從 C# 11 開始有了 required 屬性。如果欄位被標為 required,反序列化在資料沒有該欄位時可能會丟出錯誤:
public class Person
{
public string Name { get; set; }
[JsonPropertyName("Age")]
public required int Age { get; set; }
public string Address { get; set; }
}
如果舊資料裡沒有 Age 欄位——會拋出結構不符合的例外。
型別變更:int → string
// 原本是:
public class Record { public int Count; }
// 現在變成:
public class Record { public string Count; }
如果資料裡是 "Count":42,反序列化到 string 可能會成功(視情況有智慧型轉換),但反過來就可能丟例外。
移除基底類別
如果序列化的物件有繼承關係,但你改了繼承結構——舊檔案的反序列化可能會失敗,有時是「靜默」出問題、有時會明確丟例外。
6. 常見錯誤
錯誤 №1:不假思索地變動現有屬性。
重命名或改變屬性型別卻沒考慮現有的序列化資料,會導致反序列化時資訊遺失。
錯誤 №2:忘記為新欄位使用 nullable。
新屬性應該要麼是 nullable,要麼有合理的預設值。
錯誤 №3:不測試向下相容性。
改動類別後一定要測試:舊檔案/資料能否正確載入。
錯誤 №4:混用不同套件的屬性標註。
不要在同一個屬性上同時使用 JsonPropertyName 與 JsonProperty。
GO TO FULL VERSION