C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南

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# 声明类型(如 stringint)不匹配,会直接抛出 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" 或者是代表无值的空字符串 "" 时,安全地将其转换为 decimalnull

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

安全的操作规范(提取真实数据)

  1. 纯中转/序列化/拼接场景(最稳妥) :利用 Convert.ToString() 提取字面量。

    C# 复制代码
    // 完美应对 JsonElement。
    // 配合 InvariantCulture 防止部署在不同国家服务器时,小数点被转换成逗号的问题。
    string safeStringValue = Convert.ToString(model.Value, CultureInfo.InvariantCulture)!;
  2. 业务逻辑需严格执行类型判断 :通过检查 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; } 的成员,对字段直接静默忽略,不报错但数据会全部丢失。

解决方案

  1. 最佳实践:将实体类的成员强制重构为标准属性 { get; set; }

  2. 兼容方案:若存在大量历史代码难以修改,需在全局 Options 中显式开启:

    C# 复制代码
    var options = new JsonSerializerOptions { IncludeFields = true };

4. 迁移与调试的黄金法则

在未来遇到 System.Text.Json 抛出异常或反序列化出 null 时,请严格遵循以下排查步骤:

  1. 绝不盲猜数据结构 :不要依赖 API 文档。务必将 response.Content.ReadAsStreamAsync() 临时替换为 ReadAsStringAsync(),把原始 JSON 字符串完整打印到日志中,肉眼确认真实的层级和数据格式。
  2. 检查大小写配置 :确认代码中是否遗漏了 PropertyNameCaseInsensitive = true 的配置。
  3. 检查类型严格性 :排查 JSON 里的 "123" 和 C# 里的 int 是否发生了直接碰撞,若有,必须引入 Converter。
  4. 检查转换器泛型匹配 :贴在可空类型属性上的转换器,其继承的基类绝对不能是非空类型(如 JsonConverter<decimal?> 绝不能用于 decimal 属性),必须分毫不差。
相关推荐
序安InToo2 小时前
第6课|注释与代码风格
后端·操作系统·嵌入式
洋洋技术笔记2 小时前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang052 小时前
VS Code 配置 Markdown 环境
后端
navms2 小时前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang052 小时前
离线数仓的优化及重构
后端
Nyarlathotep01132 小时前
gin01:初探gin的启动
后端·go
JxWang052 小时前
安卓手机配置通用多屏协同及自动化脚本
后端
JxWang052 小时前
Windows Terminal 配置 oh-my-posh
后端
SimonKing2 小时前
OpenCode AI编程助手如何添加Skills,优化项目!
java·后端·程序员