C# JSON 序列化完全指南

C# 阅读约 14 分钟
C#JSONSystem.Text.JsonNewtonsoft.Json序列化

深入解析 C# 中 System.Text.Json 与 Newtonsoft.Json 两大 JSON 库的用法、配置技巧、AOT 支持以及从 Newtonsoft 迁移的实用指南。

System.Text.Json 与 Newtonsoft.Json 对比

.NET 生态中 JSON 序列化由两大库主导:System.Text.Json(.NET Core 3.0 起内置,与框架深度集成)和 Newtonsoft.Json(又名 Json.NET,历史最悠久的第三方 JSON 库)。两者的选择取决于具体需求和项目背景。

特性System.Text.JsonNewtonsoft.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.SerializeJsonSerializer.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 = trueDefaultIgnoreCondition = 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 类型(recordrecord struct)是推荐的 JSON 数据载体。System.Text.Json 对 record 有原生支持,通过构造函数参数名匹配属性名实现反序列化,无需额外的 [JsonConstructor](前提是参数名与 JSON 键名一致)。
  • Source Generator 项目:使用源生成器时,务必将所有参与序列化的类型及其嵌套类型在 [JsonSerializable] 中声明。遗漏某个类型在普通反射模式下只是性能回退,但在 AOT 环境中会导致运行时崩溃。

各语言 JSON API 对比

语言 序列化 反序列化 美化输出 配置方式
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize&lt;T&gt;(str) WriteIndented = true JsonSerializerOptions

常见问题 (FAQ)

JSON 序列化时如何处理日期时间类型?

大多数语言的 JSON 库默认不支持日期时间类型。通常做法是序列化为 ISO 8601 格式字符串(如 "2026-07-15T10:30:00Z")或 Unix 时间戳,反序列化时再转换回日期时间对象。Python 可通过 default 参数指定自定义编码器;JavaScript 可在 toJSON 方法中处理;Java 的 Jackson 支持 @JsonFormat 注解配置日期格式。

如何忽略 JSON 序列化中的 null 值或空字段?

不同语言的实现方式不同:Python 可在自定义编码器中过滤 None 值;JavaScript 可使用 replacer 函数或 JSON.stringify 的第二个参数;Java Jackson 用 @JsonInclude(Include.NON_NULL) 注解;Go 使用 omitempty struct tag;C# 设置 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull。

JSON 序列化时如何处理循环引用?

循环引用是 JSON 序列化的常见陷阱。标准 JSON 库通常会抛出异常或栈溢出。解决方案包括:使用 @JsonIdentityInfo 注解(Java Jackson)、实现自定义序列化器跳过反向引用、将对象图转换为 DTO 后再序列化,或使用专门的图序列化库如 flatted(JavaScript)。

为什么我的私有字段没有被序列化?

JSON 序列化库通常只序列化公开(public)字段或带有 getter 的属性。这是设计上的安全考虑。Python 的 json 模块默认只序列化 dict 的公共键;Java Jackson 可通过 @JsonProperty 注解私有字段或配置 ObjectMapper 的 Visibility;C# 中 System.Text.Json 默认只序列化公共属性,需通过 [JsonInclude] 特性包含非公共成员。

JSON 序列化性能有哪些优化技巧?

1) 复用序列化器实例(避免每次创建 ObjectMapper/JsonSerializerOptions);2) 对于大型数据,使用流式 API(如 Jackson 的 JsonGenerator / Python 的 iterencode);3) 使用编译时生成(C# Source Generator、Go easyjson);4) 避免过多嵌套层次;5) 数值类型避免不必要的大精度;6) 使用数据库分页或分块传输大 JSON。

推荐工具

以下工具可以帮助你在 C# 开发中更高效地处理 JSON 数据:

相关文章