System.Text.Json 与 Newtonsoft.Json 对比
.NET 生态中 JSON 序列化由两大库主导:System.Text.Json(.NET Core 3.0 起内置,与框架深度集成)和 Newtonsoft.Json(又名 Json.NET,历史最悠久的第三方 JSON 库)。两者的选择取决于具体需求和项目背景。
| 特性 | System.Text.Json | Newtonsoft.Json |
|---|---|---|
| 运行时性能 | 更快,内存分配更少 | 较慢,但覆盖更多功能 |
| AOT/原生裁剪 | 原生支持(源生成器) | 不支持 |
| 属性大小写敏感 | 默认严格区分大小写 | 默认不区分大小写 |
| 非公开成员访问 | 默认不支持 | 支持 OptIn / OptOut |
| 引用循环处理 | ReferenceHandler(.NET 6+) | PreserveReferencesHandling |
| JSON 注释支持 | ReadCommentHandling | 原生支持 |
| 字符串转数字宽容度 | 严格拒绝,需显式配置 | 默认允许(宽松模式) |
| 反序列化未映射成员 | 默认报错(严格模式) | 默认忽略 |
新项目应优先使用 System.Text.Json。它的性能优势来自 Utf8JsonReader/Utf8JsonWriter 底层 API 直接操作 UTF-8 字节,避免了字符串分配的开销。仅在以下场景保留或选择 Newtonsoft:需要复杂引用处理、深度自定义的契约解析器、大量依赖 JObject/JArray 的动态 JSON 操作、或者迁移遗留系统成本过高时。
核心 API 与 JsonSerializerOptions
JsonSerializer.Serialize 和 JsonSerializer.Deserialize 是 System.Text.Json 的核心入口。JsonSerializerOptions 是线程安全的配置容器,应创建后全局复用。
using System.Text.Json;
using System.Text.Json.Serialization;
var user = new User { Name = "张三", Age = 28, Email = null };
// 基础序列化:紧凑输出,PascalCase(与 C# 属性名一致)
string basicJson = JsonSerializer.Serialize(user);
Console.WriteLine(basicJson);
// {"Name":"张三","Age":28}
// 带完整配置的序列化
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
string pretty = JsonSerializer.Serialize(user, options);
Console.WriteLine(pretty);
// {
// "name": "张三",
// "age": 28
// }
// 反序列化 —— 支持从流(Stream)直接反序列化
string jsonInput = "{\"name\":\"李四\",\"age\":30}";
var parsed = JsonSerializer.Deserialize<User>(jsonInput, options);
JsonSerializerOptions 的创建成本较高(内部有反射缓存和元数据构建),但它是线程安全的,创建一次后可以被所有序列化操作共享。不要在每个请求中新建 JsonSerializerOptions 实例,这是最常见的性能反模式之一。
JsonSerializerOptions 深入配置
var options = new JsonSerializerOptions
{
// ---- 命名策略 ----
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
// 自定义蛇形命名:new JsonSnakeCaseNamingPolicy()
// ---- 空值处理 ----
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
// 也可选 WhenWritingDefault(0、false 等也忽略)
// ---- 格式化 ----
WriteIndented = true,
// ---- 容错处理 ----
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true,
// ---- 数字处理(前后端互操作的关键) ----
NumberHandling = JsonNumberHandling.AllowReadingFromString
| JsonNumberHandling.WriteAsString,
// ---- 引用处理(.NET 6+) ----
ReferenceHandler = ReferenceHandler.IgnoreCycles,
// ---- 非 ASCII 字符转义 ----
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
NumberHandling 对于前后端协作尤其重要。JavaScript 的 number 类型是 IEEE 754 双精度浮点数,无法安全表示超过 2^53 的整数。使用 WriteAsString 可以将 C# 的大整数以字符串形式输出,保证精度不丢失。ReferenceHandler.IgnoreCycles 是 .NET 6 新增的实用选项,遇到循环引用时静默截断而非抛出异常,这在与 Entity Framework 导航属性配合时非常方便。
自定义 JsonConverter
当内置的泛型转换器不能覆盖业务需求时,继承 JsonConverter<T> 实现完全自定义的读写逻辑。
using System.Text.Json;
using System.Text.Json.Serialization;
// 自定义日期转换器:输出 "yyyy-MM-dd" 格式
public class DateOnlyConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
var str = reader.GetString();
return DateTime.Parse(str!);
}
public override void Write(Utf8JsonWriter writer, DateTime value,
JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd"));
}
}
// 使用方式1:通过 JsonSerializerOptions 全局注册
var opts = new JsonSerializerOptions();
opts.Converters.Add(new DateOnlyConverter());
// 使用方式2:通过属性标记局部使用
public class Event
{
public string Title { get; set; }
[JsonConverter(typeof(DateOnlyConverter))]
public DateTime EventDate { get; set; }
}
JsonConverter<T> 针对具体的值类型进行转换。如果需要对多个相关类型(如所有时间相关类型)提供统一转换逻辑,应当实现 JsonConverterFactory。工厂类的 CanConvert 方法决定哪些目标类型适用该转换器,然后工厂动态创建对应的 JsonConverter<T> 实例。这比在每个类型上都重复定义转换器要优雅得多。
源生成器与 AOT 编译
.NET 6 引入的 JSON 源生成器在编译时生成序列化代码,消除了运行时的反射和 IL Emit,是实现 Native AOT 部署和 trim-safe 应用的核心组件。
using System.Text.Json.Serialization;
// 定义序列化上下文 —— 列出所有需要参与 JSON 处理的类型
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
[JsonSerializable(typeof(ApiResponse<User>))]
[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
internal partial class AppJsonContext : JsonSerializerContext
{
}
// 使用时通过上下文获取类型信息,而不是传递 Type
string json = JsonSerializer.Serialize(user, AppJsonContext.Default.User);
var deserialized = JsonSerializer.Deserialize(json, AppJsonContext.Default.User);
// 列表类型也需要显式声明
var users = new List<User> { user };
string listJson = JsonSerializer.Serialize(users, AppJsonContext.Default.ListUser);
源生成器的核心限制在于所有需要序列化的类型及其嵌套类型必须在编译时通过 [JsonSerializable] 特性明确列出。这要求开发者对自己的数据模型有清晰的认识。在使用源生成器的项目中,常见的反模式是遗漏了某个嵌套属性类型——编译会通过,但运行时该嵌套对象会回退到反射模式,在 AOT 环境中则直接崩溃。
Newtonsoft.Json 核心用法
对于仍在使用或需要迁移的项目,了解 Newtonsoft 的核心模式仍然必要。
using Newtonsoft.Json;
var product = new Product
{
Id = 1,
Name = "机械键盘",
Price = 299.99m,
Category = null,
Tags = new List<string> { "外设", "办公" }
};
string json = JsonConvert.SerializeObject(product, new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new Newtonsoft.Json.Serialization
.CamelCasePropertyNamesContractResolver(),
DateFormatString = "yyyy-MM-dd HH:mm:ss",
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
});
// Newtonsoft 的特性体系
public class Product
{
[JsonProperty("product_id")] // 重命名映射
public int Id { get; set; }
[JsonIgnore] // 完全忽略
public string InternalCode { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Category { get; set; }
[JsonConverter(typeof(VersionConverter))]
public Version ApiVersion { get; set; }
}
Newtonsoft 默认大小写不敏感,这在从 JavaScript 前端接收数据时是便利的,但也意味着一个属性可能匹配多个大小写变体的 JSON 键名。Newtonsoft 的 ContractResolver 体系比 System.Text.Json 的 JsonTypeInfo 更成熟灵活,但性能开销也更大。
Newtonsoft 到 System.Text.Json 迁移指南
迁移过程中以下几个方面最容易出现不兼容,需要逐一排查和适配。
// 1. 大小写敏感性 —— 最常见的首坑
// 解决:全局启用不区分大小写
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
// 2. 构造函数注入
// Newtonsoft 自动匹配非默认构造函数的参数名(不区分大小写)
// System.Text.Json 必须显式标记 [JsonConstructor]
public class User
{
public string Name { get; }
public int Age { get; }
[JsonConstructor]
public User(string name, int age) => (Name, Age) = (name, age);
}
// 3. 字符串数字互转
// Newtonsoft 默认 "42" → 42 自动转换
// System.Text.Json:显式配置
options.NumberHandling = JsonNumberHandling.AllowReadingFromString;
// 4. JObject/JArray → JsonNode/JsonDocument
// Newtonsoft: JObject.Parse(json)["key"]
// System.Text.Json: JsonNode.Parse(json)!["key"]
// 5. 类型名称处理(多态)
// Newtonsoft: TypeNameHandling.Auto
// System.Text.Json (.NET 7+): [JsonDerivedType]
[JsonDerivedType(typeof(Circle), typeDiscriminator: "circle")]
[JsonDerivedType(typeof(Square), typeDiscriminator: "square")]
public class Shape { }
迁移的推荐步骤:首先全局启用 PropertyNameCaseInsensitive = true 和 DefaultIgnoreCondition = WhenWritingNull 以接近 Newtonsoft 的默认行为;然后逐一替换 JsonConvert 调用为 JsonSerializer;接下来将 JObject/JToken 迁移为 JsonNode/JsonDocument;最后处理构造函数注入、自定义 JsonConverter 和多态类型。
ASP.NET Core 中的 JSON 配置
ASP.NET Core 从 3.0 起默认使用 System.Text.Json 处理 HTTP 请求和响应的 JSON 正文。配置通过依赖注入完成。
// Program.cs —— .NET 6+ Minimal API 风格
var builder = WebApplication.CreateBuilder(args);
// 为 Minimal API 和 MVC Controller 配置 JSON 行为
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.DefaultIgnoreCondition =
JsonIgnoreCondition.WhenWritingNull;
options.SerializerOptions.Converters.Add(
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
});
var app = builder.Build();
app.MapPost("/api/users", (User user) =>
{
// 请求体自动通过 System.Text.Json 反序列化
return Results.Ok(new { id = Guid.NewGuid(), user.Name });
});
app.Run();
对于需要恢复 Newtonsoft 的项目(如迁移过程中的过渡期),可以通过 Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包安装支持,然后在配置中调用 builder.Services.AddControllers().AddNewtonsoftJson()。不建议在新项目中同时使用两个库,这会增加依赖体积并使序列化行为难以预测。
常见陷阱与最佳实践
- 多态序列化:派生类属性在序列化时默认丢失,仅输出基类中定义的属性。.NET 7+ 使用
[JsonDerivedType]声明派生类型及鉴别器名称;更早版本需自定义JsonConverter读写类型判别字段。 - DateTime 格式:System.Text.Json 默认输出 ISO 8601 格式(含时区偏移如
+08:00)。前端 JavaScript 通常期望无时区格式或在解析时正确处理时区偏移。建议团队在 API 层面约定统一的日期格式并经自定义转换器强制执行。 - 循环引用:Entity Framework 的导航属性最常见。.NET 6+ 使用
ReferenceHandler.IgnoreCycles简单拦截;或者设计专门的 DTO 类将实体与 API 契约解耦,这是更根本的解决方案。 - 不可变类型:record 类型(
record和record struct)是推荐的 JSON 数据载体。System.Text.Json 对 record 有原生支持,通过构造函数参数名匹配属性名实现反序列化,无需额外的[JsonConstructor](前提是参数名与 JSON 键名一致)。 - Source Generator 项目:使用源生成器时,务必将所有参与序列化的类型及其嵌套类型在
[JsonSerializable]中声明。遗漏某个类型在普通反射模式下只是性能回退,但在 AOT 环境中会导致运行时崩溃。