JSON 序列化实战
大约 11 分钟约 3272 字
JSON 序列化实战
简介
JSON 是现代 Web 开发中最常用的数据交换格式。.NET 提供了 System.Text.Json(内置高性能)和 Newtonsoft.Json(功能丰富)两种序列化库。掌握 JSON 序列化/反序列化、自定义转换器、忽略策略等是 API 开发的基础技能。
System.Text.Json 从 .NET Core 3.0 开始引入,在 .NET 5/6/7/8 中不断完善。它在性能上远超 Newtonsoft.Json,内存分配更少,且支持 Native AOT。对于新项目,应优先选择 System.Text.Json。
特点
基础操作
序列化与反序列化
/// <summary>
/// JsonSerializer 基础用法
/// </summary>
// 序列化
var user = new User { Id = 1, Name = "张三", Email = "zhang@test.com" };
string json = JsonSerializer.Serialize(user);
// {"Id":1,"Name":"张三","Email":"zhang@test.com"}
// 美化输出
string pretty = JsonSerializer.Serialize(user, new JsonSerializerOptions
{
WriteIndented = true
});
// 反序列化
var deserialized = JsonSerializer.Deserialize<User>(json)!;
// 异步操作(适合大文件/网络流)
using var stream = File.OpenRead("data.json");
var data = await JsonSerializer.DeserializeAsync<List<User>>(stream);
// 反序列化为 JsonDocument(动态访问)
using var doc = JsonDocument.Parse(json);
string name = doc.RootElement.GetProperty("Name").GetString()!;
// ==========================================
// 常用 JsonSerializerOptions
// ==========================================
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // camelCase
PropertyNameCaseInsensitive = true, // 反序列化忽略大小写
WriteIndented = true, // 美化输出
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 忽略 null
NumberHandling = JsonNumberHandling.AllowReadingFromString, // 字符串数字
ReadCommentHandling = JsonCommentHandling.Skip, // 跳过注释
AllowTrailingCommas = true, // 允许尾逗号
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 中文不转义
};属性控制
/// <summary>
/// JSON 属性注解
/// </summary>
public class UserDto
{
[JsonPropertyName("user_id")]
public int Id { get; set; }
[JsonPropertyName("user_name")]
[MaxLength(50)]
public string Name { get; set; } = "";
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Email { get; set; }
[JsonIgnore]
public string Password { get; set; } = "";
[JsonPropertyOrder(1)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[JsonConverter(typeof(JsonStringEnumConverter))]
public UserRole Role { get; set; }
}
public enum UserRole
{
Admin, Editor, Viewer
}
// ==========================================
// JsonPropertyNameOrder — 控制属性顺序
// ==========================================
public class ApiResponse
{
[JsonPropertyOrder(-2)] // 最先
public bool Success { get; set; }
[JsonPropertyOrder(-1)]
public string? Message { get; set; }
[JsonPropertyOrder(0)] // 默认
public object? Data { get; set; }
[JsonPropertyOrder(1)] // 最后
public string? TraceId { get; set; }
}
// ==========================================
// JsonRequired — 必填字段验证 (.NET 7+)
// ==========================================
public class CreateOrderRequest
{
[JsonRequired]
public int ProductId { get; set; }
[JsonRequired]
[JsonPropertyName("quantity")]
public int Quantity { get; set; }
public string? Remarks { get; set; }
}
// 反序列化时缺少必填字段会抛 JsonException自定义转换器
JsonConverter
/// <summary>
/// 自定义 JSON 转换器
/// </summary>
// 日期格式转换器
public class DateTimeConverter : 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.ParseExact(reader.GetString()!, Format, CultureInfo.InvariantCulture);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format));
}
}
// 使用
[JsonConverter(typeof(DateTimeConverter))]
public DateTime CreatedAt { get; set; }
// 全局注册
var options = new JsonSerializerOptions();
options.Converters.Add(new DateTimeConverter());
options.Converters.Add(new JsonStringEnumConverter()); // 枚举转字符串高级自定义转换器
// ==========================================
// 枚举转换器 — 支持自定义值
// ==========================================
public class LowerCaseStringEnumConverter<T> : JsonConverter<T> where T : struct, Enum
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
return Enum.Parse<T>(value!, ignoreCase: true);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString().ToLowerInvariant());
}
}
// 使用: [JsonConverter(typeof(LowerCaseStringEnumConverter<UserRole>))]
// ==========================================
// DateTimeOffset 转换器 — 时间戳格式
// ==========================================
public class UnixTimestampConverter : JsonConverter<DateTimeOffset>
{
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
return DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64());
if (reader.TokenType == JsonTokenType.String)
return DateTimeOffset.Parse(reader.GetString()!);
throw new JsonException($"Unexpected token: {reader.TokenType}");
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value.ToUnixTimeSeconds());
}
}
// ==========================================
// Dictionary 转换器 — 忽略大小写的 Key
// ==========================================
public class CaseInsensitiveDictionaryConverter<TValue> : JsonConverter<Dictionary<string, TValue>>
{
public override Dictionary<string, TValue> Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var dict = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
var key = reader.GetString()!;
reader.Read();
var value = JsonSerializer.Deserialize<TValue>(ref reader, options)!;
dict[key] = value;
}
return dict;
}
public override void Write(
Utf8JsonWriter writer, Dictionary<string, TValue> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
// ==========================================
// 值对象转换器 — 包装/解包
// ==========================================
public struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
public class MoneyConverter : JsonConverter<Money>
{
public override Money Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 支持 "USD 100.50" 或 {"amount": 100.50, "currency": "USD"} 两种格式
if (reader.TokenType == JsonTokenType.String)
{
var parts = reader.GetString()!.Split(' ');
return new Money(decimal.Parse(parts[1]), parts[0]);
}
reader.Read();
var amount = 0m;
var currency = "";
while (reader.TokenType != JsonTokenType.EndObject)
{
var prop = reader.GetString()!;
reader.Read();
if (prop == "amount") amount = reader.GetDecimal();
else if (prop == "currency") currency = reader.GetString()!;
reader.Read();
}
return new Money(amount, currency);
}
public override void Write(Utf8JsonWriter writer, Money value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteNumber("amount", value.Amount);
writer.WriteString("currency", value.Currency);
writer.WriteEndObject();
}
}多态序列化
/// <summary>
/// 多态类型序列化(System.Text.Json)
/// </summary>
[JsonDerivedType(typeof(Dog), "dog")]
[JsonDerivedType(typeof(Cat), "cat")]
public abstract class Animal
{
public string Name { get; set; } = "";
}
public class Dog : Animal
{
public string Breed { get; set; } = "";
}
public class Cat : Animal
{
public bool IsIndoor { get; set; }
}
// 序列化时自动包含类型信息
var animals = new Animal[] { new Dog { Name = "旺财", Breed = "金毛" }, new Cat { Name = "咪咪", IsIndoor = true } };
string json = JsonSerializer.Serialize(animals);
// [{"$type":"dog","Name":"旺财","Breed":"金毛"},{"$type":"cat","Name":"咪咪","IsIndoor":true}]
var result = JsonSerializer.Deserialize<Animal[]>(json);
// ==========================================
// 自定义多态转换器(更灵活)
// ==========================================
public class Notification
{
public string Type { get; set; } = "";
}
public class EmailNotification : Notification
{
public string To { get; set; } = "";
public string Subject { get; set; } = "";
}
public class SmsNotification : Notification
{
public string PhoneNumber { get; set; } = "";
public string Content { get; set; } = "";
}
public class NotificationConverter : JsonConverter<Notification>
{
public override Notification Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var typeDiscriminator = doc.RootElement.GetProperty("type").GetString();
return typeDiscriminator switch
{
"email" => doc.Deserialize<EmailNotification>(options)!,
"sms" => doc.Deserialize<SmsNotification>(options)!,
_ => throw new JsonException($"Unknown notification type: {typeDiscriminator}")
};
}
public override void Write(Utf8JsonWriter writer, Notification value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}Source Generator
编译时代码生成
/// <summary>
/// System.Text.Json Source Generator
/// </summary>
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
[JsonSerializable(typeof(Order))]
public partial class MyJsonContext : JsonSerializerContext
{
}
// 使用 — 零反射,AOT 友好
string json = JsonSerializer.Serialize(user, MyJsonContext.Default.User);
var user2 = JsonSerializer.Deserialize(json, MyJsonContext.Default.User);
// 在 Minimal API 中使用
// builder.Services.ConfigureHttpJsonOptions(options =>
// {
// options.SerializerOptions.TypeInfoResolver = MyJsonContext.Default;
// });
// ==========================================
// Source Generator 选项
// ==========================================
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
GenerationMode = JsonSourceGenerationMode.Default)]
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(Order))]
public partial class AppJsonContext : JsonSerializerContext { }
// GenerationMode:
// Default — 生成序列化和反序列化代码
// Serialization — 只生成序列化代码(体积更小)
// Metadata — 只生成元数据,不生成代码动态 JSON 处理
JsonDocument 与 JsonNode
// ==========================================
// JsonDocument — 只读 DOM(高性能、低内存)
// ==========================================
using var doc = JsonDocument.Parse(jsonString);
// 安全访问
if (doc.RootElement.TryGetProperty("name", out var nameElement))
{
var name = nameElement.GetString();
}
// 遍历
foreach (var property in doc.RootElement.EnumerateObject())
{
Console.WriteLine($"{property.Name}: {property.Value}");
}
// JsonDocument 是只读的,不能修改
// 适合只读解析场景
// ==========================================
// JsonNode — 可变 DOM(.NET 6+)
// ==========================================
var node = JsonNode.Parse(jsonString)!;
// 读取
var name = node["name"]?.GetValue<string>();
var age = node["age"]?.GetValue<int>();
// 修改
node["age"] = 30;
node["address"] = new JsonObject { ["city"] = "北京", ["zip"] = "100000" };
// 添加数组
node["tags"] = new JsonArray("tag1", "tag2", "tag3");
// 删除
node["oldField"]?.Parent?.Remove();
// 转回 JSON
var modifiedJson = node.ToJsonString();
// ==========================================
// JsonNode 与强类型互转
// ==========================================
var userNode = JsonSerializer.SerializeToNode(user);
var restoredUser = userNode?.Deserialize<User>();JsonPath 风格查询
/// <summary>
/// 简单的 JsonPath 查询辅助
/// </summary>
public static class JsonPathHelper
{
// 按 path 获取值,如 "data.items[0].name"
public static JsonElement? GetByPath(this JsonElement root, string path)
{
var current = root;
var segments = path.Split('.', '[', ']')
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
foreach (var segment in segments)
{
if (int.TryParse(segment, out var index))
{
if (current.ValueKind != JsonValueKind.Array)
return null;
if (index < 0 || index >= current.GetArrayLength())
return null;
current = current[index];
}
else
{
if (!current.TryGetProperty(segment, out current))
return null;
}
}
return current;
}
}
// 使用
using var doc = JsonDocument.Parse(responseJson);
var firstName = doc.RootElement.GetByPath("data.users[0].name")?.GetString();常用技巧
条件序列化
/// <summary>
/// 条件忽略和默认值处理
/// </summary>
public class ProductDto
{
public string Name { get; set; } = "";
// 值为默认时不序列化
public decimal Price { get; set; }
// 条件忽略(方法名格式:ShouldSerialize + 属性名)
public string? Description { get; set; }
public bool ShouldSerializeDescription() => !string.IsNullOrEmpty(Description);
// 只读属性(反序列化时忽略)
public string DisplayName => $"{Name} - ¥{Price}";
}
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // camelCase
PropertyNameCaseInsensitive = true, // 反序列化忽略大小写
NumberHandling = JsonNumberHandling.AllowReadingFromString,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
};性能优化
// ==========================================
// 序列化性能最佳实践
// ==========================================
// 1. 复用 JsonSerializerOptions(不要每次 new)
// 反面: 每次请求都 new JsonSerializerOptions()
// 正面: 全局共享一个 options 实例(线程安全)
public static class JsonDefaults
{
public static readonly JsonSerializerOptions Web = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
}
// 2. 使用 Utf8JsonWriter 直接写入(零中间分配)
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream);
writer.WriteStartObject();
writer.WriteString("name", user.Name);
writer.WriteNumber("age", user.Age);
writer.WriteEndObject();
writer.Flush();
var bytes = stream.ToArray(); // UTF-8 字节,不需要再编码
// 3. 流式处理大文件
await using var fileStream = File.OpenRead("large-data.json");
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<User>(fileStream))
{
ProcessUser(item); // 逐条处理,不一次性加载全部
}
// 4. Source Generator(最大性能提升)
var json = JsonSerializer.Serialize(user, MyJsonContext.Default.User);
// 比反射序列化快 2-5 倍,AOT 兼容错误处理
// ==========================================
// 处理 JSON 反序列化错误
// ==========================================
try
{
var user = JsonSerializer.Deserialize<User>(jsonString, options);
}
catch (JsonException ex)
{
Console.WriteLine($"JSON 解析错误: {ex.Message}");
Console.WriteLine($"路径: {ex.Path}"); // 出错的 JSON 路径
Console.WriteLine($"行号: {ex.LineNumber}"); // 行号
Console.WriteLine($"位置: {ex.BytePositionInLine}"); // 列号
}
// 自定义错误处理 — 跳过无效元素
public class TolerantListConverter<T> : JsonConverter<List<T>>
{
public override List<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var list = new List<T>();
if (reader.TokenType != JsonTokenType.StartArray)
return list;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
try
{
var item = JsonSerializer.Deserialize<T>(ref reader, options);
if (item is not null)
list.Add(item);
}
catch (JsonException)
{
// 跳过无效元素,记录警告
Console.WriteLine($"警告: 跳过无效的 JSON 元素");
}
}
return list;
}
public override void Write(Utf8JsonWriter writer, List<T> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}优点
缺点
总结
JSON 序列化推荐使用 System.Text.Json(性能好、AOT 友好)。核心用法:JsonPropertyName 重命名、JsonIgnore 忽略、JsonConverter 自定义转换。性能敏感场景用 Source Generator。从 Newtonsoft 迁移时注意默认行为差异(严格模式、大小写敏感)。
核心原则:
- 优先 System.Text.Json — 新项目不要用 Newtonsoft
- Source Generator — 性能敏感或 AOT 场景必用
- 复用 Options — 不要每次 new JsonSerializerOptions
- 中文不转义 — 使用 UnsafeRelaxedJsonEscaping
- 错误处理 — 记录 Path 和行号方便排查
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道"为什么这样写"和"在什么边界下不能这样写"。
- 设备接入类主题通常同时涉及协议、线程、实时刷新和异常恢复。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
- 明确采集周期、重连策略、数据缓存和状态同步方式。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
- 只验证通信成功,不验证断线、抖动和异常包。
- 每次序列化都 new JsonSerializerOptions(应复用)。
- 大文件反序列化一次性加载到内存(应用流式处理)。
- 忘记处理中文转义问题(默认会转义非 ASCII 字符)。
- JsonNode 在高并发场景下不适用(非线程安全)。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
- 继续补齐设备模拟、离线回放、现场诊断和配置中心能力。
- 学习 Utf8JsonWriter 的底层写入机制。
适用场景
- 当你准备把《JSON 序列化实战》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了"高级"而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
- 检查 JsonException 的 Path 属性定位 JSON 路径。
复盘问题
- 如果把《JSON 序列化实战》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《JSON 序列化实战》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《JSON 序列化实战》最大的收益和代价分别是什么?
