1. 核心设计哲学差异
在进行代码迁移前,必须牢记这两个库在底层设计哲学上的根本分歧,这是几乎所有反序列化报错的根源:
- Newtonsoft.Json (主打兼容与灵活) :它非常宽容,会尽最大努力去猜测你的意图,在底层默默帮你做各种隐式的类型转换和容错处理。
- System.Text.Json (主打性能与安全) :微软为了追求极致的执行效率而原生打造。它极其严格,要求 JSON 数据结构和 C# 模型"严丝合缝",绝不会越界替你做任何类型转换。
2. 基础特性与配置替换对照表
| 场景 | Newtonsoft.Json (旧) | System.Text.Json (新) | 迁移备注 |
|---|---|---|---|
| 指定 JSON 键名 | [JsonProperty("name")] |
[JsonPropertyName("name")] |
必须逐个替换。 |
| 忽略某字段 | [JsonIgnore] |
[JsonIgnore] |
基本一致。 |
| 忽略空值 (Null) | NullValueHandling.Ignore |
全局 Options: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull |
推荐在全局 JsonSerializerOptions 中统一配置,减少序列化体积。 |
| 忽略默认值 | DefaultValueHandling.Ignore |
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] |
新版将其合并到了 JsonIgnore 特性中。 |
3. 四大高频"踩坑"重灾区及标准解决方案
🚨 坑一:大小写严格敏感 (Case Sensitivity)
问题描述 :很多第三方 API 返回的小驼峰命名(如 userProfile),而 C# 模型是大驼峰命名(如 UserProfile)。老版能完美自动映射,新版只要大小写不一致,直接反序列化为 null。
解决方案 :在反序列化时,务必全局传入配置允许忽略大小写:
C#
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var result = JsonSerializer.Deserialize<MyModel>(jsonString, options);
🚨 坑二:基础类型严格匹配(数字与字符串的鸿沟)
问题描述 :对接外部不可控 API 时经常遇到格式不规范的数据。比如 ID 字段有时是数字 ("id": 123),有时是字符串 ("id": "A-123");金额字段有时返回字符串 ("price": "19.99"),甚至用空字符串表示无数据 ("discount": "")。
新版只要遇到 JSON 节点类型与 C# 声明类型(如 string 对 int)不匹配,会直接抛出 JsonException 崩溃。
解决方案 :不要指望内置配置项能完美兜底(尤其是处理空字符串),建议直接封装自定义 JsonConverter。
🛠️ 通用工具 1:数字安全转字符串转换器 (NumberToStringConverter)
用途 :当 C# 模型定义为 string Id,但外部 JSON 传入的是数字 123 时,自动将其转换为 "123" 且不报错。
C#
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace YourNamespace.Helpers
{
public class NumberToStringConverter : JsonConverter<string?>
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number) return reader.GetInt64().ToString();
if (reader.TokenType == JsonTokenType.String) return reader.GetString();
return null;
}
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
if (value == null) writer.WriteNullValue();
else writer.WriteStringValue(value);
}
}
}
// 实体类使用方式:[JsonConverter(typeof(NumberToStringConverter))]
🛠️ 通用工具 2:字符串安全转可空金额转换器 (StringToDecimalConverter)
用途 :当 C# 模型定义为 decimal? Price,但 JSON 传入的是 "19.99" 或者是代表无值的空字符串 "" 时,安全地将其转换为 decimal 或 null。
C#
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace YourNamespace.Helpers
{
// 注意:泛型必须与属性类型完全一致(此处为可空类型 decimal?)
public class StringToDecimalConverter : JsonConverter<decimal?>
{
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number) return reader.GetDecimal();
if (reader.TokenType == JsonTokenType.String)
{
string? strValue = reader.GetString();
// 很多老旧 API 喜欢用空字符串代表没有值,安全处理为 null
if (string.IsNullOrWhiteSpace(strValue)) return null;
if (decimal.TryParse(strValue, out decimal result)) return result;
}
return null;
}
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
{
// 序列化时,可根据对接方 API 的偏好决定是否转回字符串
if (value.HasValue) writer.WriteStringValue(value.Value.ToString("0.00"));
else writer.WriteNullValue();
}
}
}
// 实体类使用方式:[JsonConverter(typeof(StringToDecimalConverter))]
// 警告:此转换器必须配合 public decimal? Price { get; set; } 使用!不可用于非空 decimal。
🚨 坑三:动态类型 object 的解析陷阱
问题描述 :当模型中存在 public object Value { get; set; }(例如用于接收不确定结构的数据、扩展字段 metadata 等),老版会猜测并转化为 string, int 等具体 C# 基础类型。而新版会统一将其解析为 JsonElement 结构体。
危险操作 :任何试图将反序列化后的 object 强转回基础类型的操作都会导致运行时崩溃!
- ❌
string val = (string)model.Value;-> 抛出 InvalidCastException - ❌
if (model.Value is string s)-> 永远为 false - ❌
string val = model.Value as string;-> 永远返回 null
安全的操作规范(提取真实数据) :
-
纯中转/序列化/拼接场景(最稳妥) :利用
Convert.ToString()提取字面量。C#// 完美应对 JsonElement。 // 配合 InvariantCulture 防止部署在不同国家服务器时,小数点被转换成逗号的问题。 string safeStringValue = Convert.ToString(model.Value, CultureInfo.InvariantCulture)!; -
业务逻辑需严格执行类型判断 :通过检查
JsonElement.ValueKind。C#if (model.Value is JsonElement element) { if (element.ValueKind == JsonValueKind.String) string s = element.GetString(); else if (element.ValueKind == JsonValueKind.Number) decimal d = element.GetDecimal(); }
🚨 坑四:字段 (Fields) 被静默忽略
问题描述 :老版会自动序列化和反序列化 public string name; 这种公开的字段 (Fields)。新版默认只处理属性 (Properties) ,即带有 { get; set; } 的成员,对字段直接静默忽略,不报错但数据会全部丢失。
解决方案:
-
最佳实践:将实体类的成员强制重构为标准属性
{ get; set; }。 -
兼容方案:若存在大量历史代码难以修改,需在全局 Options 中显式开启:
C#var options = new JsonSerializerOptions { IncludeFields = true };
4. 迁移与调试的黄金法则
在未来遇到 System.Text.Json 抛出异常或反序列化出 null 时,请严格遵循以下排查步骤:
- 绝不盲猜数据结构 :不要依赖 API 文档。务必将
response.Content.ReadAsStreamAsync()临时替换为ReadAsStringAsync(),把原始 JSON 字符串完整打印到日志中,肉眼确认真实的层级和数据格式。 - 检查大小写配置 :确认代码中是否遗漏了
PropertyNameCaseInsensitive = true的配置。 - 检查类型严格性 :排查 JSON 里的
"123"和 C# 里的int是否发生了直接碰撞,若有,必须引入 Converter。 - 检查转换器泛型匹配 :贴在可空类型属性上的转换器,其继承的基类绝对不能是非空类型(如
JsonConverter<decimal?>绝不能用于decimal属性),必须分毫不差。