从 Newtonsoft.Json 迁移到 System.Text.Json

本文演示如何从 Newtonsoft.Json 迁移到 System.Text.Json

System.Text.Json 命名空间提供用于序列化和反序列化 JavaScript 对象表示法 (JSON) 的功能。 System.Text.Json 库包含在 .NET Core 3.1 和更高版本的运行时中。 对于其他目标框架,请安装 System.Text.Json NuGet 包。 包支持以下框架:

  • .NET Standard 2.0 及更高版本
  • .NET Framework 4.6.2 及更高版本
  • .NET Core 2.0、2.1 和 2.2

提示

可以使用 GitHub Copilot 的 AI 帮助Newtonsoft.Json 迁移

System.Text.Json 主要关注性能、安全性和标准符合性。 它在默认行为方面有一些重要差异,不打算具有与 Newtonsoft.Json 相同的功能。 对于某些方案,System.Text.Json 当前没有内置功能,但有建议解决方法。 对于其他方案,解决方法是不切实际的。

System.Text.Json 团队正在投入精力,添加最常需要的功能。 如果你的应用程序依赖于缺少的功能,请考虑在 dotnet/runtime GitHub 存储库上提交问题,来了解是否可添加对你的方案的支持。

本文的大部分内容介绍如何使用 JsonSerializer API,不过也包含有关如何使用 JsonDocument(表示文档对象模型或 DOM)、Utf8JsonReaderUtf8JsonWriter 类型的指导。

在 Visual Basic 中,不能使用 Utf8JsonReader,这也意味着无法编写自定义转换器。 这里介绍的大多数解决方法都要求编写自定义转换器。 可使用 C# 编写自定义转换器,并将其注册到 Visual Basic 项目中。 有关详细信息,请参阅 Visual Basic 支持

差异表

下表列出 Newtonsoft.Json 功能和 System.Text.Json 等效功能。 这些等效功能分为以下类别:

  • ✔️ 内置功能支持。 从 System.Text.Json 获取类似行为可能会需要使用特性或全局选项。
  • ⚠️ 不受支持,但可能有解决方法。 解决方法是自定义转换器,它们可能无法提供与 Newtonsoft.Json 功能完全相同的功能。 对于其中一些功能,提供示例代码作为示例。 如果你依赖于这些 Newtonsoft.Json 功能,迁移需要修改 .NET 对象模型或进行其他代码更改。
  • ❌ 不受支持,解决方法不可行或无法提供。 如果你依赖于这些 Newtonsoft.Json 功能,则无法在不进行重大更改的情况下进行迁移。
Newtonsoft.Json 功能 System.Text.Json 等效
默认情况下不区分大小写的反序列化 ✔️ PropertyNameCaseInsensitive 全局设置
Camel 大小写属性名称 ✔️ PropertyNamingPolicy 全局设置
对属性名称采用蛇形命名法 ✔️ 蛇形命名法命名策略
最小字符转义 ✔️ 严格字符转义,可配置
NullValueHandling.Ignore 全局设置 ✔️ DefaultIgnoreCondition 全局选项
允许注释 ✔️ ReadCommentHandling 全局设置
允许尾随逗号 ✔️ AllowTrailingCommas 全局设置
自定义转换器注册 ✔️ 优先级顺序不同
默认最大深度为 64,可配置 ✔️ 默认最大深度为 64,可配置
PreserveReferencesHandling 全局设置 ✔️ ReferenceHandling 全局设置
序列化或反序列化带引号的数字 ✔️ NumberHandling 全局设置,[JsonNumberHandling] 特性
反序列化为不可变类和结构 ✔️ JsonConstructor,C# 9 记录
支持字段 ✔️ IncludeFields 全局设置,[JsonInclude] 特性
DefaultValueHandling 全局设置 ✔️ DefaultIgnoreCondition 全局设置
[JsonProperty] 上的 NullValueHandling 设置 ✔️ JsonIgnore 特性
[JsonProperty] 上的 DefaultValueHandling 设置 ✔️ JsonIgnore 特性
反序列化具有非字符串键的 Dictionary ✔️ 受支持
支持非公共属性资源库和 Getter ✔️ JsonInclude 特性
[JsonConstructor] 特性 ✔️ [JsonConstructor] 特性
ReferenceLoopHandling 全局设置 ✔️ ReferenceHandling 全局设置
回调 ✔️ 回调
NaN、Infinity、-Infinity ✔️ 受支持
[JsonProperty] 特性上的 Required 设置 ✔️ [JsonRequired] 特性和 C# 必需的修饰符
DefaultContractResolver 用于忽略属性 ✔️ DefaultJsonTypeInfoResolver 类
多态序列化 ✔️ [JsonDerivedType] 特性
多态反序列化 ✔️ [JsonDerivedType] 特性上的类型鉴别器
反序列化字符串枚举值 ✔️ 反序列化字符串枚举值
MissingMemberHandling 全局设置 ✔️ 处理缺少的成员
在没有资源库的情况下填充属性 ✔️ 在没有资源库的情况下填充属性
ObjectCreationHandling 全局设置 ✔️ 重用而不是替换属性
支持范围广泛的类型 ⚠️ 某些类型需要自定义转换器
将推断类型反序列化为 object 属性 ⚠️ 不受支持,解决方法,示例
将 JSON null 文本反序列化为不可为 null 的值类型 ⚠️ 不受支持,解决方法,示例
DateTimeZoneHandlingDateFormatString 设置 ⚠️ 不受支持,解决方法,示例
JsonConvert.PopulateObject 方法 ⚠️ 不受支持,解决方法
支持 System.Runtime.Serialization 特性 ⚠️ 不受支持,解决方法,示例
JsonObjectAttribute ⚠️ 不受支持,解决方法
允许不带引号的属性名称 设计上不受支持
字符串值前后允许单引号 设计上不受支持
对字符串属性允许非字符串 JSON 值 设计上不受支持
TypeNameHandling.All 全局设置 设计上不受支持
支持 JsonPath 查询 不支持
可配置的限制 不支持

这不是 Newtonsoft.Json 功能的详尽列表。 此列表包含在 GitHub 问题StackOverflow 文章中请求的许多方案。 如果对此处所列且当前没有示例代码的一个方案实现了解决方法,并且如果要共享解决方案,请在本页底部的“反馈”部分选择“此页面”。 这会在本文档的 GitHub 存储库中创建一个问题,并将它也列在此页面上的“反馈”部分中。

默认行为的差异

System.Text.Json 在默认情况下十分严格,避免代表调用方进行任何猜测或解释,强调确定性行为。 该库是为了实现性能和安全性而特意这样设计的。 Newtonsoft.Json 默认情况下十分灵活。 设计中的这种根本差异是默认行为中以下许多特定差异的背后原因。

不区分大小写的反序列化

在反序列化过程中,默认情况下 Newtonsoft.Json 进行不区分大小写的属性名称匹配。 System.Text.Json 默认值区分大小写,这可提供更好的性能,因为它执行精确匹配。 有关如何执行不区分大小写的匹配的信息,请参阅不区分大小写的属性匹配

如果使用 ASP.NET Core 间接使用 System.Text.Json,则无需执行任何操作即可获得类似于 Newtonsoft.Json 的行为。 ASP.NET Core 在使用 System.Text.Json 时,会为 camel 大小写属性名称和不区分大小写的匹配指定设置。

默认情况下,ASP.NET Core 还允许反序列化带引号的数字

最小字符转义

在序列化过程中,Newtonsoft.Json 对于让字符通过而不进行转义相对宽松。 也就是说,它不会将它们替换为 \uxxxx(其中 xxxx 是字符的码位)。 对字符进行转义时,它会通过在字符前发出 \ 来实现此目的(例如," 会变为 \")。 System.Text.Json 会在默认情况下转义较多字符,以对跨站点脚本 (XSS) 或信息泄露攻击提供深度防御保护,并使用六字符序列执行此操作。 System.Text.Json 会在默认情况下转义所有非 ASCII 字符,因此如果在 Newtonsoft.Json 中使用 StringEscapeHandling.EscapeNonAscii,则无需执行任何操作。 System.Text.Json 在默认情况下还会转义 HTML 敏感字符。 有关如何替代默认 System.Text.Json 行为的信息,请参阅自定义字符编码

注释

在反序列化过程中,Newtonsoft.Json 在默认情况下会忽略 JSON 中的注释。 System.Text.Json 默认值是对注释引发异常,因为 RFC 8259 规范不包含它们。 有关如何允许注释的信息,请参阅允许注释和尾随逗号

尾随逗号

在反序列化过程中,默认情况下 Newtonsoft.Json 会忽略尾随逗号。 它还会忽略多个尾随逗号(例如 [{"Color":"Red"},{"Color":"Green"},,])。 System.Text.Json 默认值是对尾随逗号引发异常,因为 RFC 8259 规范不允许使用它们。 有关如何使 System.Text.Json 接受它们的信息,请参阅允许注释和尾随逗号。 无法允许多个尾随逗号。

转换器注册优先级

自定义转换器的 Newtonsoft.Json 注册优先级如下所示:

  • 属性上的特性
  • 类型上的特性
  • 转换器 集合

此顺序意味着 Converters 集合中的自定义转换器会由通过在类型级别应用特性而注册的转换器替代。 这两个注册都会由属性级别的特性替代。

自定义转换器的 System.Text.Json 注册优先级是不同的:

  • 属性上的特性
  • Converters 集合
  • 类型上的特性

此处的差别在于 Converters 集合中的自定义转换器会替代类型级别的特性。 此优先级顺序的目的是使运行时更改替代设计时选项。 无法更改优先级。

有关自定义转换器注册的详细信息,请参阅注册自定义转换器

最大深度

默认情况下,Newtonsoft.Json 的最新版本的最大深度限制为 64。 System.Text.Json 的默认限制也是 64,可通过设置 JsonSerializerOptions.MaxDepth 进行配置。

如果使用 ASP.NET Core 时间接使用 System.Text.Json,则默认的最大深度限制为 32。 默认值与模型绑定的默认值相同,并且在 JsonOptions 类中设置。

JSON 字符串(属性名称和字符串值)

在反序列化过程中,Newtonsoft.Json 接受用双引号、单引号括起来或不带引号的属性名称。 它接受用双引号或单引号括起来的字符串值。 例如,Newtonsoft.Json 接受以下 JSON:

{
  "name1": "value",
  'name2': "value",
  name3: 'value'
}

System.Text.Json 仅接受双引号中的属性名称和字符串值,因为 RFC 8259 规范要求使用该格式,这是唯一视为有效 JSON 的格式。

用单引号括起来的值会导致 JsonException,并出现以下消息:

''' is an invalid start of a value.

字符串属性的非字符串值

Newtonsoft.Json 接受非字符串值(如数字或文本 truefalse),以便反序列化为类型字符串的属性。 下面是 Newtonsoft.Json 成功反序列化为以下类的 JSON 示例:

{
  "String1": 1,
  "String2": true,
  "String3": false
}
public class ExampleClass
{
    public string String1 { get; set; }
    public string String2 { get; set; }
    public string String3 { get; set; }
}

System.Text.Json 不将非字符串值反序列化为字符串属性。 字符串字段接收的非字符串值会导致 JsonException,并出现以下消息:

The JSON value could not be converted to System.String.

使用 JsonSerializer 的方案

下面一部分方案不受内置功能支持,但有解决方法可用。 解决方法是自定义转换器,它们可能无法提供与 Newtonsoft.Json 功能完全相同的功能。 对于其中一些功能,提供示例代码作为示例。 如果你依赖于这些 Newtonsoft.Json 功能,迁移需要修改 .NET 对象模型或进行其他代码更改。

对于下面的一部分方案,解决方法不可行或无法提供。 如果你依赖于这些 Newtonsoft.Json 功能,则无法在不进行重大更改的情况下进行迁移。

允许或写入带引号的数字

Newtonsoft.Json 可以序列化或反序列化由 JSON 字符串表示的数字(括在引号中)。 例如,它可以接受 {"DegreesCelsius":"23"} 而不是 {"DegreesCelsius":23}。 若要在 System.Text.Json 中启用该行为,请将 JsonSerializerOptions.NumberHandling 设置为 WriteAsStringAllowReadingFromString,或使用 [JsonNumberHandling] 特性。

如果使用 ASP.NET Core 间接使用 System.Text.Json,则无需执行任何操作即可获得类似于 Newtonsoft.Json 的行为。 ASP.NET Core 在使用 System.Text.Json 时指定 Web 默认值,Web 默认值允许带引号的数字。

有关详细信息,请参阅允许或写入带引号的数字

指定要在反序列化时使用的构造函数

使用 Newtonsoft.Json [JsonConstructor] 特性可以指定在反序列化为 POCO 时要调用的构造函数。

System.Text.Json 还具有 [JsonConstructor] 特性。 有关详细信息,请参阅不可变类型和记录

有条件地忽略属性

Newtonsoft.Json 有多种方法可在序列化或反序列化时有条件地忽略属性:

  • DefaultContractResolver 使你可以基于任意条件选择要包含或忽略的属性。
  • JsonSerializerSettings 上的 NullValueHandlingDefaultValueHandling 设置使你指定应忽略所有 null 值或默认值属性。
  • [JsonProperty] 特性上的 NullValueHandlingDefaultValueHandling 设置使你可以指定在设置为 null 或默认值时应忽略的单个属性。

System.Text.Json 提供以下方法,用于在序列化期间忽略属性或字段:

此外,在 .NET 7 及更高版本中,可以自定义 JSON 协定以忽略基于任意条件的属性。 有关详细信息,请参阅自定义协定

公共和非公共字段

Newtonsoft.Json 可以序列化和反序列化字段以及属性。

System.Text.Json 中,在序列化或反序列化时,使用 JsonSerializerOptions.IncludeFields 全局设置或 [JsonInclude] 特性来包含公共字段。 有关示例,请参阅包含字段

保留对象引用并处理循环

默认情况下,Newtonsoft.Json 按值进行序列化。 例如,如果对象包含两个属性,而这些属性包含对同一个 Person 对象的引用,该 Person 对象属性的值会在 JSON 重复。

Newtonsoft.JsonJsonSerializerSettings 上有一个 PreserveReferencesHandling 设置,可让你按引用进行序列化:

  • 标识符元数据会添加到为第一个 Person 对象创建的 JSON。
  • 为第二个 Person 对象创建的 JSON 包含对该标识符(而不是属性值)的引用。

Newtonsoft.Json 还具有一个 ReferenceLoopHandling 设置,使你可以忽略循环引用,而不是引发异常。

若要在 System.Text.Json 中保留引用并处理循环引用,请将 JsonSerializerOptions.ReferenceHandler 设置为 PreserveReferenceHandler.Preserve 设置等效于 Newtonsoft.Json 中的 PreserveReferencesHandling = PreserveReferencesHandling.All

ReferenceHandler.IgnoreCycles 选项的行为类似于 Newtonsoft.JsonReferenceLoopHandling.Ignore。 一个区别是,System.Text.Json 实现将引用循环替换为 null JSON 标记,而不是忽略对象引用。 有关更多信息,请参阅忽略循环引用

与 Newtonsoft.JsonReferenceResolver 一样,System.Text.Json.Serialization.ReferenceResolver 类定义在序列化和反序列化过程中保留引用的行为。 创建派生类以指定自定义行为。 有关示例,请参阅 GuidReferenceResolver

一些相关的 Newtonsoft.Json 功能不受支持:

有关详细信息,请参阅保留引用并处理循环引用

包含非字符串键的字典

Newtonsoft.JsonSystem.Text.Json 都支持 Dictionary<TKey, TValue> 类型的集合。 但是在 System.Text.Json 中,TKey 必须是基元类型,而不是自定义类型。 有关详细信息,请参阅支持的键类型

注意

如果反序列化为 Dictionary<TKey, TValue>(其中 TKey 采用 string 以外的任何类型),则可能会导致使用应用程序中出现安全漏洞。 有关详细信息,请参阅 dotnet/runtime#4761

没有内置支持的类型

System.Text.Json 不为以下类型提供内置支持:

对于没有内置支持的类型,可以实现自定义转换器。

多态序列化

Newtonsoft.Json 会自动执行多态序列化。 从 .NET 7 开始,System.Text.Json 支持通过 JsonDerivedTypeAttribute 特性执行多态序列化。 有关详细信息,请参阅序列化派生类的属性

多态反序列化

Newtonsoft.Json 具有 TypeNameHandling 设置,它在序列化期间将类型名称元数据添加到 JSON。 它在反序列化期间使用元数据执行多态反序列化。 从 .NET 7 开始,System.Text.Json 依赖于类型鉴别器信息来执行多态反序列化。 此元数据在 JSON 中发出,然后在反序列化期间用于确定是反序列化为基类型还是派生类型。 有关详细信息,请参阅序列化派生类的属性

若要在旧 .NET 版本中支持多态反序列化,请创建一个转换器,如如何编写自定义转换器中的示例。

反序列化字符串枚举值

默认情况下,System.Text.Json 不支持反序列化字符串枚举值,而 Newtonsoft.Json 则支持。 例如,以下代码会引发 JsonException

string json = "{ \"Text\": \"Hello\", \"Enum\": \"Two\" }";
var _ = JsonSerializer.Deserialize<MyObj>(json); // Throws exception.

class MyObj
{
    public string Text { get; set; } = "";
    public MyEnum Enum { get; set; }
}

enum MyEnum
{
    One,
    Two,
    Three
}

但是,你可以使用 JsonStringEnumConverter 转换器启用字符串枚举值的反序列化。 有关详细信息,请参阅枚举作为字符串

对象属性的反序列化

Newtonsoft.Json 反序列化为 Object 时,它会:

  • 推断 JSON 有效负载中的基元值的类型(不是 null),并以装箱对象的形式返回存储的 stringlongdoublebooleanDateTime。 “基元值”是单个 JSON 值,如 JSON 数字、字符串、truefalsenull
  • 为 JSON 有效负载中的复杂值返回 JObjectJArray。 “复杂值”是括在大括号 ({}) 中的 JSON 键值对的集合或括在方括号 ([]) 中的值的列表。 括在大括号或方括号中的属性和值可以具有附加属性或值。
  • 当有效负载具有 null JSON 文本时,返回空引用。

System.Text.Json 在每次反序列化为 Object 时,为基元和复数值存储装箱 JsonElement,例如:

  • object 属性。
  • object 字典值。
  • object 数组值。
  • object

但是,System.Text.Json 处理 null 的方式与 Newtonsoft.Json 相同,会在有效负载中包含 null JSON 文本时返回空引用。

若要为 object 属性实现类型推理,请创建转换器(类似于如何编写自定义转换器中的示例)。

将 null 反序列化为不可为 null 的类型

Newtonsoft.Json 在以下方案中不会引发异常:

  • NullValueHandling 设置为 Ignore,并且
  • 在反序列化过程中,JSON 对于不可为 null 的值类型包含 null 值。

在相同方案中,System.Text.Json 会引发异常。 (System.Text.Json 中对应的 null 处理设置为 JsonSerializerOptions.IgnoreNullValues = true。)

如果你拥有目标类型,在最佳解决方法是使相关属性可为 null(例如,将 int 更改为 int?)。

另一种解决方法是为类型创建转换器,如以下为 DateTimeOffset 类型处理 null 值的示例:

using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DateTimeOffsetNullHandlingConverter : JsonConverter<DateTimeOffset>
    {
        public override DateTimeOffset Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
            reader.TokenType == JsonTokenType.Null
                ? default
                : reader.GetDateTimeOffset();

        public override void Write(
            Utf8JsonWriter writer,
            DateTimeOffset dateTimeValue,
            JsonSerializerOptions options) =>
            writer.WriteStringValue(dateTimeValue);
    }
}

通过对属性使用特性或是通过向 Converters 集合添加转换器来注册此自定义转换器。

注意:前面的转换器处理 null 值的方式与 Newtonsoft.Json 为指定默认值的 POCO 进行处理的方式不同。 例如,假设以下代码表示目标对象:

public class WeatherForecastWithDefault
{
    public WeatherForecastWithDefault()
    {
        Date = DateTimeOffset.Parse("2001-01-01");
        Summary = "No summary";
    }
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

并且假设使用前面的转换器反序列化以下 JSON:

{
  "Date": null,
  "TemperatureCelsius": 25,
  "Summary": null
}

反序列化之后,Date 属性具有 1/1/0001 (default(DateTimeOffset)),即,在构造函数中设置的值会被覆盖。 给定相同 POCO 和 JSON,Newtonsoft.Json 反序列化会将 1/1/2001 保留在 Date 属性中。

反序列化为不可变类和结构

Newtonsoft.Json 可以反序列化为不可变类和结构,因为它可以使用具有参数的构造函数。

System.Text.Json 中,使用 [JsonConstructor] 特性来指定参数化构造函数的用法。 C# 9 记录也是不可变的,并且支持作为反序列化目标。 有关详细信息,请参阅不可变类型和记录

必需的属性

Newtonsoft.Json 中,通过对 [JsonProperty] 特性设置 Required 来指定属性是必需的。 如果在 JSON 中没有为标记为必需的属性收到值,Newtonsoft.Json 会引发异常。

从 .NET 7 开始,可以在必需属性上使用 C# required 修饰符或 JsonRequiredAttribute 特性。 如果 JSON 有效负载不包含标记属性的值,System.Text.Json 将引发异常。 有关详细信息,请参阅必需属性

指定日期格式

Newtonsoft.Json 提供多种方法来控制如何序列化和反序列化 DateTimeDateTimeOffset 类型的属性:

  • DateTimeZoneHandling 设置可用于将所有 DateTime 值序列化为 UTC 日期。
  • DateFormatString 设置和 DateTime 转换器可用于自定义日期字符串的格式。

System.Text.Json 支持 ISO 8601-1:2019,包括 RFC 3339 配置文件。 此格式被广泛采用,无歧义,并且精确地进行往返。 若要使用任何其他格式,请创建自定义转换器。 例如,以下转换器会序列化和反序列化使用 Unix epoch 格式的 JSON,而无论是否带有时区偏移(/Date(1590863400000-0700)//Date(1590863400000)/ 之类的值):

sealed class UnixEpochDateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
    static readonly DateTimeOffset s_epoch = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
    static readonly Regex s_regex = new("^/Date\\(([+-]*\\d+)([+-])(\\d{2})(\\d{2})\\)/$", RegexOptions.CultureInvariant);

    public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string formatted = reader.GetString()!;
        Match match = s_regex.Match(formatted);

        if (
                !match.Success
                || !long.TryParse(match.Groups[1].Value, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out long unixTime)
                || !int.TryParse(match.Groups[3].Value, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out int hours)
                || !int.TryParse(match.Groups[4].Value, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out int minutes))
        {
            throw new JsonException();
        }

        int sign = match.Groups[2].Value[0] == '+' ? 1 : -1;
        TimeSpan utcOffset = new(hours * sign, minutes * sign, 0);

        return s_epoch.AddMilliseconds(unixTime).ToOffset(utcOffset);
    }

    public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
    {
        long unixTime = Convert.ToInt64((value - s_epoch).TotalMilliseconds);
        TimeSpan utcOffset = value.Offset;

        string formatted = string.Create(CultureInfo.InvariantCulture, $"/Date({unixTime}{(utcOffset >= TimeSpan.Zero ? "+" : "-")}{utcOffset:hhmm})/");

        writer.WriteStringValue(formatted);
    }
}
sealed class UnixEpochDateTimeConverter : JsonConverter<DateTime>
{
    static readonly DateTime s_epoch = new(1970, 1, 1, 0, 0, 0);
    static readonly Regex s_regex = new("^/Date\\(([+-]*\\d+)\\)/$", RegexOptions.CultureInvariant);

    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string formatted = reader.GetString()!;
        Match match = s_regex.Match(formatted);

        if (
                !match.Success
                || !long.TryParse(match.Groups[1].Value, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out long unixTime))
        {
            throw new JsonException();
        }

        return s_epoch.AddMilliseconds(unixTime);
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        long unixTime = Convert.ToInt64((value - s_epoch).TotalMilliseconds);

        string formatted = string.Create(CultureInfo.InvariantCulture, $"/Date({unixTime})/");
        writer.WriteStringValue(formatted);
    }
}

有关详细信息,请参阅 System.Text.Json 中的 DateTime 和 DateTimeOffset 支持

回调

Newtonsoft.Json 使你可以在序列化或反序列化过程中的多个点执行自定义代码:

  • OnDeserializing(开始反序列化对象时)
  • OnDeserialized(对象反序列化完成时)
  • OnSerializing(开始序列化对象时)
  • OnSerialized(对象序列化完成时)

System.Text.Json 在序列化和反序列化期间公开相同的通知。 若要使用它们,请从 System.Text.Json.Serialization 命名空间实现以下一个或多个接口:

以下示例会检查 null 属性,并在序列化和反序列化开始和结束时编写消息:

using System.Text.Json;
using System.Text.Json.Serialization;

namespace Callbacks
{
    public class WeatherForecast : 
        IJsonOnDeserializing, IJsonOnDeserialized, 
        IJsonOnSerializing, IJsonOnSerialized
    {
        public DateTime Date { get; set; }
        public int TemperatureCelsius { get; set; }
        public string? Summary { get; set; }

        void IJsonOnDeserializing.OnDeserializing() => Console.WriteLine("\nBegin deserializing");
        void IJsonOnDeserialized.OnDeserialized()
        {
            Validate();
            Console.WriteLine("Finished deserializing");
        }
        void IJsonOnSerializing.OnSerializing()
        {
            Console.WriteLine("Begin serializing");
            Validate();
        }
        void IJsonOnSerialized.OnSerialized() => Console.WriteLine("Finished serializing");

        private void Validate()
        {
            if (Summary is null)
            {
                Console.WriteLine("The 'Summary' property is 'null'.");
            }
        }
    }

    public class Program
    {
        public static void Main()
        {
            var weatherForecast = new WeatherForecast
            {
                Date = DateTime.Parse("2019-08-01"),
                TemperatureCelsius = 25,
            };

            string jsonString = JsonSerializer.Serialize(weatherForecast);
            Console.WriteLine(jsonString);

            weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString);
            Console.WriteLine($"Date={weatherForecast?.Date}");
            Console.WriteLine($"TemperatureCelsius={weatherForecast?.TemperatureCelsius}");
            Console.WriteLine($"Summary={weatherForecast?.Summary}");
        }
    }
}
// output:
//Begin serializing
//The 'Summary' property is 'null'.
//Finished serializing
//{"Date":"2019-08-01T00:00:00","TemperatureCelsius":25,"Summary":null}

//Begin deserializing
//The 'Summary' property is 'null'.
//Finished deserializing
//Date=8/1/2019 12:00:00 AM
//TemperatureCelsius = 25
//Summary=

OnDeserializing 代码无权访问新 POCO 实例。 若要在反序列化开始时操作新 POCO 实例,请将该代码放入 POCO 构造函数中。

非公共属性资源库和 Getter

Newtonsoft.Json 可以通过 JsonProperty 特性使用私有和内部属性 setter 和 getter。

System.Text.Json 支持通过 [JsonInclude] 特性使用私有和内部属性资源库和 Getter。 有关示例代码,请参阅非公共属性访问器

填充现有对象

Newtonsoft.Json 中的 JsonConvert.PopulateObject 方法将 JSON 文档反序列化为类的现有实例,而不是创建新实例。 System.Text.Json 始终使用默认公共无参数构造函数创建目标类型的新实例。 自定义转换器可以反序列化为现有实例。

重用而不是替换属性

从 .NET 8 开始, System.Text.Json 支持重用初始化的属性,而不是替换它们。 行为存在一些差异,可以在 API 建议中了解这些差异。

有关详细信息,请参阅填充初始化的属性

在没有资源库的情况下填充属性

从 .NET 8 开始,System.Text.Json 支持填充属性,包括没有资源库的属性。 有关详细信息,请参阅填充初始化的属性

蛇形命名法命名策略

System.Text.Json 包括蛇形命名法的内置命名策略。 但是,对于某些输入,Newtonsoft.Json 存在一些行为差异。 下表显示了使用 JsonNamingPolicy.SnakeCaseLower 策略转换输入时的一些差异。

输入 Newtonsoft.Json 结果 System.Text.Json 结果
"AB1" "a_b1" "ab1"
"SHA512Managed" "sh_a512_managed" "sha512_managed"
"abc123DEF456" "abc123_de_f456" "abc123_def456"
"KEBAB-CASE" "keba_b-_case" "kebab-case"

System.Runtime.Serialization 特性

使用 DataContractAttributeDataMemberAttributeIgnoreDataMemberAttributeSystem.Runtime.Serialization 属性可以定义数据协定。 “数据协定”是在服务与客户端之间达成的正式协议,用于以抽象方式描述要交换的数据。 数据协定精确定义要序列化哪些属性以便交换。

System.Text.Json 没有对这些属性的内置支持。 但是,从 .NET 7 开始,可以使用自定义类型解析程序来添加支持。 有关示例,请参阅 ZCS.DataContractResolver

八进制数字

Newtonsoft.Json 将带前导零的数字视为八进制数字。 System.Text.Json 不允许存在前导零,因为 RFC 8259 规范不允许。

处理所有缺少的成员

如果正在反序列化的 JSON 包含目标类型中缺少的属性,则可以将 Newtonsoft.Json 配置为引发异常。 默认情况下,System.Text.Json 会忽略 JSON 中的额外属性,但在使用 [JsonExtensionData] 特性时除外。

在 .NET 8 及更高版本中,可以使用以下方式之一来设置是否跳过或禁止未映射 JSON 属性的首选项:

JsonObjectAttribute

Newtonsoft.Json 具有属性 JsonObjectAttribute,可在类型级别应用该属性,用于控制序列化哪些成员、如何处理 null 值以及是否需要所有成员。 System.Text.Json 没有可应用于类型的等效属性。 对于某些行为(例如 null 值处理),可以在全局 JsonSerializerOptions 或每个属性上单独配置相同的行为。

请考虑以下示例,该示例使用 Newtonsoft.Json.JsonObjectAttribute 指定应忽略所有 null 属性:

[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class Person { ... }

在 System.Text.Json 中,可以设置所有类型和属性的行为:

JsonSerializerOptions options = new()
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

string json = JsonSerializer.Serialize<Person>(person, options);

或者,可以单独设置每个属性的行为

public class Person
{
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string? Name { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public int? Age { get; set; }
}

接下来,请考虑以下示例,该示例使用 Newtonsoft.Json.JsonObjectAttribute 指定所有成员属性必须存在于 JSON 中:

[JsonObject(ItemRequired = Required.Always)]
public class Person { ... }

你可以通过将 C# required 修饰符或 JsonRequiredAttribute 添加到每个属性,在 System.Text.Json 中实现相同的行为。 有关详细信息,请参阅必需属性

public class Person
{
    [JsonRequired]
    public string? Name { get; set; }

    public required int? Age { get; set; }
}

TraceWriter

Newtonsoft.Json 使你可以使用 TraceWriter 进行调试,以查看序列化或反序列化所生成的日志。 System.Text.Json 不执行日志记录。

与 JToken(如 JObject、JArray)相比的 JsonDocument 和 JsonElement

System.Text.Json.JsonDocument 提供从现有 JSON 有效负载分析和生成只读文档对象模型 (DOM) 的功能。 DOM 提供对 JSON 有效负载中的数据的随机访问。 可以通过 JsonElement 类型访问构成有效负载的 JSON 元素。 JsonElement 类型提供用于将 JSON 文本转换为常见 .NET 类型的 API。 JsonDocument 公开了 RootElement 属性。

从 .NET 6 开始,可通过使用 System.Text.Json.Nodes 命名空间中的 JsonNode 类型和其他类型,从现有 JSON 有效负载分析和生成可变 DOM。 有关详细信息,请参阅使用 JsonNode

JsonDocument 为 IDisposable

JsonDocument 将内存中的数据视图生成到共用缓冲区中。 因此,与 Newtonsoft.Json 中的 JObjectJArray 不同,JsonDocument 类型实现 IDisposable 并且需要在 using 块中使用。 有关详细信息,请参阅 JsonDocument 是 IDisposable

JsonDocument 为只读

System.Text.Json DOM 无法添加、删除或修改 JSON 元素。 它这样设计是为了实现性能,并减少用于分析常见 JSON 有效负载大小(即 < 1 MB)的分配。

JsonElement 是联合结构

JsonDocumentRootElement 公开为类型 JsonElement 的属性,该类型是包含任何 JSON 元素的联合结构类型。 Newtonsoft.Json 使用专用分层类型,如 JObjectJArrayJToken 等。 JsonElement 是可以搜索和枚举的内容,你可以使用 JsonElement 将 JSON 元素具体化为 .NET 类型。

从 .NET 6 开始,可使用 JsonNode 类型,以及 System.Text.Json.Nodes 命名空间中与 JObjectJArrayJToken 对应的类型。 有关详细信息,请参阅使用 JsonNode

如何搜索子元素的 JsonDocument 和 JsonElement

使用 Newtonsoft.Json 中的 JObjectJArray 搜索 JSON 令牌的速度往往相对较快,因为它们是在某个字典中查找。 相比之下,若要对 JsonElement 进行搜索,需要对属性进行线性搜索,因此速度相对较慢(例如在使用 TryGetProperty 时)。 System.Text.Json 旨在最大程度减少初始分析时间,而不是查找时间。 有关详细信息,请参阅如何搜索子元素的 JsonDocument 和 JsonElement

Utf8JsonReader 与JsonTextReader

System.Text.Json.Utf8JsonReader 是面向 UTF-8 编码 JSON 文本的一个高性能、低分配的只进读取器,从 ReadOnlySpan<byte>ReadOnlySequence<byte> 读取信息。 Utf8JsonReader 是一种低级类型,可用于生成自定义分析器和反序列化程序。

Utf8JsonReader 是 ref struct

Newtonsoft.Json 中的 JsonTextReader 是一个类。 Utf8JsonReader 类型的不同之处在于它是一个 ref struct。 有关详细信息,请参阅 Utf8JsonReader 的 ref 结构限制

将 null 值读取到可为 null 的值类型中

Newtonsoft.Json 提供返回 Nullable<T> 的 API,如 ReadAsBoolean(它通过返回 bool? 来处理 Null TokenType)。 内置 System.Text.Json API 仅返回不可为 null 的值类型。 有关详细信息,请参阅将 null 值读取到可为 null 的值类型

用于读取 JSON 的多目标

如果需要继续为某些目标框架使用 Newtonsoft.Json,则可以使用多目标,并具有两种实现。 但是,这并非易事,需要进行一些 #ifdefs 和源文件复制。 要共享尽可能多代码,一种方法是围绕 Utf8JsonReaderNewtonsoft.Json.JsonTextReader 创建 ref struct 包装器。 此包装器会统一公共外围应用,同时隔离行为差异。 这使你可以隔离主要对类型的构造进行的更改,以及按引用传递新类型。 下面是 Microsoft.Extensions.DependencyModel 库遵循的模式:

Utf8JsonWriter 与JsonTextWriter

System.Text.Json.Utf8JsonWriter 是一种高性能方式,从常见 .NET 类型(例如,StringInt32DateTime)编写 UTF-8 编码的 JSON 文本。 该编写器是一种低级类型,可用于生成自定义序列化程序。

编写原始值

Newtonsoft.Json 具有 WriteRawValue 方法,它编写需要值的原始 JSON。 System.Text.Json 具有直接等效项:Utf8JsonWriter.WriteRawValue。 有关详细信息,请参阅写入原始 JSON

自定义 JSON 格式

JsonTextWriter 包含以下设置(Utf8JsonWriter 对于它们没有等效项):

  • QuoteChar - 指定要用于围绕字符串值的字符。 Utf8JsonWriter 始终使用双引号。
  • QuoteName - 指定是否要使用引号围绕属性名称。 Utf8JsonWriter 始终使用引号围绕它们。

从 .NET 9 开始,可使用 JsonWriterOptions 结构公开的选项自定义 Utf8JsonWriter 的缩进字符和大小:

JsonTextWriter 包含以下设置(Utf8JsonWriter 对于它们没有等效项):

  • 缩进 - 指定要缩进的字符数。 Utf8JsonWriter 始终缩进 2 个字符。
  • IndentChar - 指定要用于缩进的字符。 Utf8JsonWriter 始终使用空格。
  • QuoteChar - 指定要用于围绕字符串值的字符。 Utf8JsonWriter 始终使用双引号。
  • QuoteName - 指定是否要使用引号围绕属性名称。 Utf8JsonWriter 始终使用引号围绕它们。

没有解决方法可让你自定义 Utf8JsonWriter 以这些方式生成的 JSON。

编写 Timespan、Uri 或 char 值

JsonTextWriter 提供 WriteValue 方法以用于 TimeSpan、Uri 和 char 值。 Utf8JsonWriter 没有等效方法。 而是将这些值格式化为字符串(例如,通过调用 ToString())并调用 WriteStringValue

用于写入 JSON 的多目标

如果需要继续为某些目标框架使用 Newtonsoft.Json,则可以使用多目标,并具有两种实现。 但是,这并非易事,需要进行一些 #ifdefs 和源文件复制。 共享尽可能多代码的一种方法是围绕 Utf8JsonWriterNewtonsoft.Json.JsonTextWriter 创建包装器。 此包装器会统一公共外围应用,同时隔离行为差异。 这使你可以隔离主要对类型的构造进行的更改。 Microsoft.Extensions.DependencyModel 库遵循:

不支持 TypeNameHandling.All

故意从 System.Text.Json 中排除了 TypeNameHandling.All 的等效功能。 允许 JSON 有效负载指定自己的类型信息是导致 Web 应用程序中出现漏洞的常见原因。 具体来说,通过使用 TypeNameHandling.All 配置 Newtonsoft.Json,远程客户端可在 JSON 有效负载自身内部嵌入整个可执行应用程序,因此在反序列化过程中,Web 应用程序会提取并运行嵌入代码。 有关详细信息,请参阅 Friday the 13th JSON attacks PowerPoint(13 日周五的 JSON 攻击 PowerPoint)和 Friday the 13th JSON attacks details(13 日周五的 JSON 攻击细节)。

不支持 JSON 路径查询

JsonDocument DOM 不支持使用“JSON 路径”进行查询。

JsonNode DOM 中,每个 JsonNode 实例都有一个 GetPath 方法,它会返回该节点的路径。 但是,没有内置 API 来处理基于 JSON 路径查询字符串的查询。

有关详细信息,请参阅 GitHub 问题 dotnet/runtime #31068

某些限制不可配置

System.Text.Json 设置了对某些值来说无法更改的限制,例如最大标记大小以字符为单位时为 166 MB,以 base 64 为单位时为 125 MB。 有关详细信息,请参阅JsonConstants源代码和 GitHub 问题 dotnet/runtime #39953

NaN、Infinity、-Infinity

Newtonsoft 会分析 NaNInfinity-Infinity JSON 字符串标记。 对于 System.Text.Json,请使用 JsonNumberHandling.AllowNamedFloatingPointLiterals。 若要了解如何使用此设置,请参阅允许或写入引号中的数字

使用 GitHub Copilot 迁移

可以从 GitHub Copilot 获取编码帮助,以在 IDE 中转换代码。

如果使用的是 Visual Studio 2022 版本 17.8 或更高版本,则可以尝试通过 Visual Studio 中 AI 驱动的 GitHub Copilot 来帮助迁移 Newtonsoft.Json 代码以使用 System.Text.Json。 在 Copilot Chat 窗口中提交问题和要转换为提示的代码,如下例所示。

注意

GitHub Copilot 由 AI 提供支持,因此可能会带来意外和错误。 请确保验证任何生成的代码或建议。 有关 GitHub Copilot 的常规用途、产品影响、人工监督和隐私的更多信息,请参阅 GitHub Copilot 常见问题解答

Copilot Chat 示例提示

convert the following code to use System.Text.Json
Product product = new Product();

product.Name = "Apple";
product.ExpiryDate = new DateTime(2024, 08, 08);
product.Price = 3.99M;
product.Sizes = new string[] { "Small", "Medium", "Large" };

string output = JsonConvert.SerializeObject(product);
Console.WriteLine(output);

Copilot Chat 示例响应

To convert the code to use System.Text.Json, you can use the JsonSerializer class. Here's the updated code:
using System;
using System.Text.Json;

public class Product
{
    public string Name { get; set; }
    public DateTime ExpiryDate { get; set; }
    public decimal Price { get; set; }
    public string[] Sizes { get; set; }
}

public class Program
{
    public static void Main()
    {
        Product product = new Product();

        product.Name = "Apple";
        product.ExpiryDate = new DateTime(2024, 08, 08);
        product.Price = 3.99M;
        product.Sizes = new string[] { "Small", "Medium", "Large" };

        string output = JsonSerializer.Serialize(product);
        Console.WriteLine(output);
    }
}
This code uses System.Text.Json.JsonSerializer.Serialize to convert the Product object to a JSON string.

请注意,结果可能与这些示例响应中显示的结果不同。 AI 模型不确定,这意味着当被问及相同的问题时,它们可以返回不同的响应。 这可能是由于一段时间内进行的额外学习和调整、语言变体、上下文中的更改(如聊天历史记录等)。

显示在 Visual Studio 中使用 GitHub Copilot Chat 从 newtonsoft 迁移的动画屏幕截图

可以使用聊天功能(如斜杠命令、引用和线程)来设置意向,并通过已限定范围上下文获取更好的答案。 例如,如果代码文件 filename 在 IDE 中打开,则可以使用“转换 #filename 以使用 System.Text.Json”在 Copilot Chat 的提示中引用文件。 或者,可以在聊天窗口或内联聊天中使用“转换 @workspace 以使用 System.Text.Json”来引用解决方案

其他资源