把一个老项目从.NET Framework迁到.NET Core,最容易翻车的环节往往不是依赖包,而是JSON序列化。ASP.NET Core默认换成了System.Text.Json,很多接口的返回结果直接变了样:字段首字母变小写、日期格式不对、枚举变成了数字,甚至反序列化直接抛异常。这篇文章就来把System.Text.Json和Newtonsoft.Json的差异讲清楚,并给出几种实用的兼容方案。

一、两者的核心差异到底在哪里
System.Text.Json是微软为了性能重新打造的一套序列化库,设计上偏严格和保守,很多Newtonsoft.Json里的“默认宽容行为”都被砍掉了。最典型的就是命名策略:System.Text.Json默认按属性原名输出(PascalCase),而Newtonsoft.Json在ASP.NET Core的Web场景里,默认使用camelCase,属性UserName会被序列化成userName。前端拿到的字段名变了,界面上的数据绑定自然就挂了。
第二个差异是大小写敏感性。Newtonsoft.Json反序列化时默认不区分大小写,而System.Text.Json在查找属性时严格区分大小写,前端传的username匹配不到UserName属性就直接丢弃,而且不报错,这类问题排查起来特别隐蔽。第三个差异是功能支持,早期版本不支持字段序列化、不支持循环引用处理、不支持JsonIgnoreCondition的灵活配置、对Dictionary的非字符串key处理也不一样。虽然新版本逐步补齐了能力,但默认行为依旧不同。
还有日期和枚举的处理。Newtonsoft.Json默认把DateTime序列化成ISO 8601格式的本地时间字符串,而System.Text.Json严格按照ISO 8601-1标准输出,时区后缀、精度上都有细微差别。枚举方面,两者默认都输出数字,但Newtonsoft.Json提供StringEnumConverter全局转换很方便,System.Text.Json需要显式配置JsonStringEnumConverter。下面这张表总结了常见差异:
| 特性 | Newtonsoft.Json | System.Text.Json |
|---|---|---|
| 属性命名 | Web默认camelCase | 默认保持原名 |
| 反序列化大小写 | 不敏感 | 敏感(可配置) |
| 字段(Field)序列化 | 默认支持 | 需设置IncludeFields |
| 循环引用 | 默认支持(含$id) | 需显式开启ReferenceHandler |
| 忽略null属性 | NullValueHandling.Ignore | DefaultIgnoreCondition |
二、通过配置让System.Text.Json行为对齐Newtonsoft.Json
如果打算留在System.Text.Json体系里,最干净的做法是用JsonSerializerOptions把各项行为调整到和Newtonsoft.Json一致。在ASP.NET Core中,可以在Program.cs里全局配置,这样所有Controller的输入输出都会走同一套规则,不需要在每个类上加特性。
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
var jsonOptions = options.JsonSerializerOptions;
// 属性名转为camelCase,与Newtonsoft.Json Web默认行为一致
jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
// 反序列化时属性名不区分大小写
jsonOptions.PropertyNameCaseInsensitive = true;
// 序列化公共字段
jsonOptions.IncludeFields = true;
// 枚举序列化为字符串
jsonOptions.Converters.Add(new JsonStringEnumConverter());
// 忽略值为null的属性
jsonOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
// 处理循环引用
jsonOptions.ReferenceHandler = ReferenceHandler.Preserve;
// 允许注释和尾随逗号,解析更宽容
jsonOptions.ReadCommentHandling = JsonCommentHandling.Skip;
jsonOptions.AllowTrailingCommas = true;
});需要注意几点细节。ReferenceHandler.Preserve会在输出里加上$id和$values这样的元数据字段,如果前端是全新对接的还好,但如果是老系统对接,这些额外的字段可能导致解析失败,这时要评估是否真的存在循环引用场景,没有就干脆不开启,让DTO设计保持树形结构。
另外,枚举的字符串化还有个坑:如果枚举成员带有EnumMember或自定义名称,JsonStringEnumConverter默认按成员原名输出,可以用它的构造函数指定命名策略,例如new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)。日期格式如果需要完全自定义,比如输出yyyy-MM-dd HH:mm:ss这种中文系统常见的格式,就得写自定义Converter,后面会讲到写法。
三、直接换回Newtonsoft.Json:全局注册方案
如果项目里存在大量依赖Newtonsoft.Json特性的遗留代码,比如JsonProperty、JsonConverter特性满天飞,逐个改造的成本太高,更务实的方案是让ASP.NET Core整体切换回Newtonsoft.Json。微软官方提供了兼容包,安装后在管道里替换即可。
// 安装包:Microsoft.AspNetCore.Mvc.NewtonsoftJson
builder.Services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver();
// 忽略null属性
options.SerializerSettings.NullValueHandling =
NullValueHandling.Ignore;
// 日期格式
options.SerializerSettings.DateFormatString =
"yyyy-MM-dd HH:mm:ss";
// 枚举转字符串
options.SerializerSettings.Converters.Add(
new StringEnumConverter());
});这个方案的优点是迁移成本几乎为零,老代码里的特性、自定义Converter、序列化设置都能原样工作。缺点是性能上不如System.Text.Json,尤其是高并发场景下差距会被放大;同时Newtonsoft.Json的维护重心已经转向维护模式,新特性基本不会再加,长远来看还是要逐步迁移。
还有一点容易被忽略:混用两个库时要小心命名空间冲突。一个项目里既有Newtonsoft.Json.JsonProperty又有System.Text.Json.Serialization.JsonPropertyName时,两个库互不识别对方的特性,同一个类在两套序列化器下行为可能完全不同。建议通过文件级命名空间别名或者统一项目规范来约束,避免同一个DTO被两条序列化管线处理出两种结果。
四、自定义Converter处理特殊类型
无论选哪套方案,总会遇到内置能力覆盖不了的场景,比如把DateTime输出成yyyy-MM-dd HH:mm:ss、把decimal统一保留两位小数、给long类型防精度丢失转字符串等。System.Text.Json提供了JsonConverter<T>基类,实现起来并不复杂。
public class DateTimeFormatConverter : JsonConverter<DateTime>
{
private const string Format = "yyyy-MM-dd HH:mm:ss";
public override DateTime Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
return DateTime.Parse(reader.GetString());
}
public override void Write(
Utf8JsonWriter writer,
DateTime value,
JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format));
}
}
// 使用方式一:全局注册
options.Converters.Add(new DateTimeFormatConverter());
// 使用方式二:针对单个属性标注
public class Order
{
[JsonConverter(typeof(DateTimeFormatConverter))]
public DateTime CreateTime { get; set; }
}写自定义Converter时有几个注意点。Read方法里拿到的是原始token,务必做好异常处理,因为输入数据不受你控制,一个格式错误的日期就会让整个反序列化失败,建议捕获后抛出JsonException,让框架按标准的反序列化错误流程处理。另外,从.NET 6开始,如果只需要简单包装,可以使用工厂模式的Converter,避免为每个泛型类型都写一个类。
对于Newtonsoft.Json,对应的机制是继承JsonConverter并重写ReadJson和WriteJson,思路完全一致,只是API形态不同。如果团队里两套库并存,可以考虑把特殊类型的转换逻辑抽到公共层,用适配器分别包装,这样切换底层库时业务逻辑不用动。
五、迁移建议与排查思路
结合实际项目经验,给出一个推荐路径:新项目直接用System.Text.Json并按需配置,不要为了习惯去装Newtonsoft.Json;存量项目迁移时,可以先用AddNewtonsoftJson整体切换保证功能不回退,然后按模块逐步改造DTO,用JsonPropertyName替代JsonProperty,用JsonConverter替代Newtonsoft的自定义转换器,最后移除依赖。
排查序列化问题时,优先对比原始JSON字符串而不是看前端表现。可以用JsonSerializer.Serialize在单元测试里直接输出结果,和迁移前的报文做diff,字段名、大小写、日期格式、null处理这几项差异一眼就能看出来。循环引用导致的栈溢出、多态反序列化拿不到具体类型(System.Text.Json需要配合JsonDerivedType特性声明子类),这两个是迁移中报错率最高的问题,提前在测试环境覆盖到。
总的来说,System.Text.Json和Newtonsoft.Json的兼容问题不难解,关键是分清场景:要么通过配置对齐行为,要么整体替换序列化器,再辅以自定义Converter处理特殊类型。把序列化规则统一收敛到全局配置层,避免到处散落特性标注,后续的维护成本会低很多。
System.Text.JsonNewtonsoft.JsonJSON序列化修改时间:2026-09-05 01:56:41