将 Newtonsoft.Json 与 System.Text.Json 比较,并迁移到 System.Text.Json

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

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

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

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

我们正在投入精力,添加最常被要求的功能。 如果你的应用程序依赖于缺少的功能,请考虑在 dotnet/runtime GitHub 存储库上提交问题,来了解是否可添加对你的方案的支持。 请参阅长篇故事问题 #43620,了解已计划的内容。

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

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

介绍 Newtonsoft.Json 与 System.Text.Json 之间差异的表格

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

  • ✔️ 受内置功能支持。 从 System.Text.Json 获取类似行为可能需要使用特性或全局选项。
  • ⚠️ 不支持,但可以解决此问题。 解决方法是自定义转换器,它们可能无法提供与 功能完全相同的功能。 对于其中一些功能,提供示例代码作为示例。 如果你依赖于这些 Newtonsoft.Json 功能,迁移需要修改 .NET 对象模型或进行其他代码更改。
  • ❌ 不支持,解决方法不切实际或不可行。 如果你依赖于这些 Newtonsoft.Json 功能,则无法在不进行重大更改的情况下进行迁移。
Newtonsoft.Json 功能 System.Text.Json 等效
默认情况下不区分大小写的反序列化 ✔️ PropertyNameCaseInsensitive 全局设置
Camel 大小写属性名称 ✔️ PropertyNamingPolicy 全局设置
最小字符转义 ✔️ 严格字符转义,可配置
NullValueHandling.Ignore 全局设置 ✔️ DefaultIgnoreCondition 全局选项
允许注释 ✔️ ReadCommentHandling 全局设置
允许尾随逗号 ✔️ AllowTrailingCommas 全局设置
自定义转换器注册 ✔️ 优先级顺序不同
默认情况下无最大深度 ✔️ 默认最大深度为 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] 特性上的类型鉴别器
支持范围广泛的类型 ⚠️ ⚠
将推断类型反序列化为 object 属性 ⚠️ ⚠
将 JSON null 文本反序列化为不可为 null 的值类型 ⚠️ ⚠
DateTimeZoneHandlingDateFormatString 设置 ⚠️ ⚠
JsonConvert.PopulateObject 方法 ⚠️ ⚠
ObjectCreationHandling 全局设置 ⚠️ ⚠
在不带 setter 的情况下添加到集合 ⚠️ ⚠
对属性名称采用蛇形命名法 ⚠️ ⚠
支持 System.Runtime.Serialization 特性 ❌❌
MissingMemberHandling 全局设置 ❌❌
允许不带引号的属性名称 ❌❌
字符串值前后允许单引号 ❌❌
对字符串属性允许非字符串 JSON 值 ❌❌
TypeNameHandling.All 全局设置 ❌❌
支持 JsonPath 查询 ❌❌
可配置的限制 ❌❌
Newtonsoft.Json 功能 System.Text.Json 等效
默认情况下不区分大小写的反序列化 ✔️ PropertyNameCaseInsensitive 全局设置
Camel 大小写属性名称 ✔️ PropertyNamingPolicy 全局设置
最小字符转义 ✔️ 严格字符转义,可配置
NullValueHandling.Ignore 全局设置 ✔️ DefaultIgnoreCondition 全局选项
允许注释 ✔️ ReadCommentHandling 全局设置
允许尾随逗号 ✔️ AllowTrailingCommas 全局设置
自定义转换器注册 ✔️ 优先级顺序不同
默认情况下无最大深度 ✔️ 默认最大深度为 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 ✔️ 受支持
支持范围广泛的类型 ⚠️ ⚠
多态序列化 ⚠️ ⚠
多态反序列化 ⚠️ ⚠
将推断类型反序列化为 object 属性 ⚠️ ⚠
将 JSON null 文本反序列化为不可为 null 的值类型 ⚠️ ⚠
[JsonProperty] 特性上的 Required 设置 ⚠️ ⚠
DefaultContractResolver 用于忽略属性 ⚠️ ⚠
DateTimeZoneHandlingDateFormatString 设置 ⚠️ ⚠
JsonConvert.PopulateObject 方法 ⚠️ ⚠
ObjectCreationHandling 全局设置 ⚠️ ⚠
在不带 setter 的情况下添加到集合 ⚠️ ⚠
对属性名称采用蛇形命名法 ⚠️ ⚠
支持 System.Runtime.Serialization 特性 ❌❌
MissingMemberHandling 全局设置 ❌❌
允许不带引号的属性名称 ❌❌
字符串值前后允许单引号 ❌❌
对字符串属性允许非字符串 JSON 值 ❌❌
TypeNameHandling.All 全局设置 ❌❌
支持 JsonPath 查询 ❌❌
可配置的限制 ❌❌
Newtonsoft.Json 功能 System.Text.Json 等效
默认情况下不区分大小写的反序列化 ✔️ PropertyNameCaseInsensitive 全局设置
Camel 大小写属性名称 ✔️ PropertyNamingPolicy 全局设置
最小字符转义 ✔️ 严格字符转义,可配置
NullValueHandling.Ignore 全局设置 ✔️ IgnoreNullValues 全局选项
允许注释 ✔️ ReadCommentHandling 全局设置
允许尾随逗号 ✔️ AllowTrailingCommas 全局设置
自定义转换器注册 ✔️ 优先级顺序不同
默认情况下无最大深度 ✔️ 默认最大深度为 64,可配置
支持范围广泛的类型 ⚠️ ⚠
将字符串反序列化为数字 ⚠️ ⚠
反序列化具有非字符串键的 Dictionary ⚠️ ⚠
多态序列化 ⚠️ ⚠
多态反序列化 ⚠️ ⚠
将推断类型反序列化为 object 属性 ⚠️ ⚠
将 JSON null 文本反序列化为不可为 null 的值类型 ⚠️ ⚠
反序列化为不可变类和结构 ⚠️ ⚠
[JsonConstructor] 特性 ⚠️ ⚠
[JsonProperty] 特性上的 Required 设置 ⚠️ ⚠
[JsonProperty] 特性上的 NullValueHandling 设置 ⚠️ ⚠
[JsonProperty] 特性上的 DefaultValueHandling 设置 ⚠️ ⚠
DefaultValueHandling 全局设置 ⚠️ ⚠
DefaultContractResolver 用于忽略属性 ⚠️ ⚠
DateTimeZoneHandlingDateFormatString 设置 ⚠️ ⚠
回调 ⚠️ ⚠
支持公共和非公共字段 ⚠️ ⚠
支持非公共属性资源库和 Getter ⚠️ ⚠
JsonConvert.PopulateObject 方法 ⚠️ ⚠
ObjectCreationHandling 全局设置 ⚠️ ⚠
在不带 setter 的情况下添加到集合 ⚠️ ⚠
对属性名称采用蛇形命名法 ⚠️ ⚠
NaN、Infinity、-Infinity ⚠️ ⚠
PreserveReferencesHandling 全局设置 ❌❌
ReferenceLoopHandling 全局设置 ❌❌
支持 System.Runtime.Serialization 特性 ❌❌
MissingMemberHandling 全局设置 ❌❌
允许不带引号的属性名称 ❌❌
字符串值前后允许单引号 ❌❌
对字符串属性允许非字符串 JSON 值 ❌❌
TypeNameHandling.All 全局设置 ❌❌
支持 JsonPath 查询 ❌❌
可配置的限制 ❌❌

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

默认 JsonSerializer 行为相较于 Newtonsoft.Json 的差异

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

不区分大小写的反序列化

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

如果使用 ASP.NET Core 间接使用 System.Text.Json,则无需执行任何操作即可获得类似于 Newtonsoft.Json 的行为。 ASP.NET Core 在使用 时,会为 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 行为的信息,请参阅System.Text.Json

注释

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

尾随逗号

在反序列化过程中,默认情况下 Newtonsoft.Json 会忽略尾随逗号。 它还会忽略多个尾随逗号(例如 [{"Color":"Red"},{"Color":"Green"},,])。 System.Text.Json 默认值是对尾随逗号引发异常,因为 System.Text.Json 规范不允许使用它们。 有关如何使 System.Text.Json 接受它们的信息,请参阅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 仅接受双引号中的属性名称和字符串值,因为 System.Text.Json 规范要求使用该格式,这是唯一视为有效 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 功能,迁移需要修改 .NET 对象模型或进行其他代码更改。

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

允许或写入带引号的数字

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

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

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

Newtonsoft.Json 可以序列化或反序列化由 JSON 字符串表示的数字(括在引号中)。 例如,它可以接受 {"DegreesCelsius":"23"} 而不是 {"DegreesCelsius":23}。 若要在 .NET Core 3.1 的 System.Text.Json 中启用该行为,请实现类似于以下示例的自定义转换器。 该转换器处理定义为 long 的属性:

  • 它将这些属性序列化为 JSON 字符串。
  • 它在反序列化期间接受 JSON 数字和括在引号中的数字。
using System.Buffers;
using System.Buffers.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class LongToStringConverter : JsonConverter<long>
    {
        public override long Read(
            ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.String)
            {
                ReadOnlySpan<byte> span =
                    reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;

                if (Utf8Parser.TryParse(span, out long number, out int bytesConsumed) &&
                    span.Length == bytesConsumed)
                {
                    return number;
                }

                if (long.TryParse(reader.GetString(), out number))
                {
                    return number;
                }
            }

            return reader.GetInt64();
        }

        public override void Write(
            Utf8JsonWriter writer, long longValue, JsonSerializerOptions options) =>
            writer.WriteStringValue(longValue.ToString());
    }
}

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

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

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

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

.NET Core 3.1 中的 System.Text.Json 仅支持无参数构造函数。 作为一种解决方法,可以在自定义转换器中调用所需的任何构造函数。 请参阅反序列化为不可变类和结构的示例。

有条件地忽略属性

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

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

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

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

.NET Core 3.1 中的 System.Text.Json 提供以下方法,用于在序列化期间忽略属性:

这些选项“不”支持根据在运行时计算的任意条件忽略所选属性。

此外,在 .NET Core 3.1 中,不能:

  • 忽略具有类型的默认值的所有属性。
  • 忽略具有类型的默认值的所选属性。
  • 忽略值为 null 的所选属性。
  • 基于运行时计算的任意条件忽略所选属性。

对于该功能,可以编写自定义转换器。 下面是一个示例 POCO 和一个适用于它的自定义转换器,用于说明此方法:

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class WeatherForecastRuntimeIgnoreConverter : JsonConverter<WeatherForecast>
    {
        public override WeatherForecast Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            var wf = new WeatherForecast();

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return wf;
                }

                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    string propertyName = reader.GetString()!;
                    reader.Read();
                    switch (propertyName)
                    {
                        case "Date":
                            DateTimeOffset date = reader.GetDateTimeOffset();
                            wf.Date = date;
                            break;
                        case "TemperatureCelsius":
                            int temperatureCelsius = reader.GetInt32();
                            wf.TemperatureCelsius = temperatureCelsius;
                            break;
                        case "Summary":
                            string summary = reader.GetString()!;
                            wf.Summary = string.IsNullOrWhiteSpace(summary) ? "N/A" : summary;
                            break;
                    }
                }
            }

            throw new JsonException();
        }

        public override void Write(Utf8JsonWriter writer, WeatherForecast wf, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            writer.WriteString("Date", wf.Date);
            writer.WriteNumber("TemperatureCelsius", wf.TemperatureCelsius);
            if (!string.IsNullOrWhiteSpace(wf.Summary) && wf.Summary != "N/A")
            {
                writer.WriteString("Summary", wf.Summary);
            }

            writer.WriteEndObject();
        }
    }
}

如果值为 null、空字符串或 "N/A",则转换器会导致从序列化中省略 Summary 属性。

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

此方法在以下情况下需要其他逻辑:

  • POCO 包含复杂属性。
  • 需要处理特性(如 [JsonIgnore])或选项(如自定义编码器)。

公共和非公共字段

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

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

.NET Core 3.1 中的 System.Text.Json 仅适用于公共属性。 自定义转换器可提供此功能。

保留对象引用并处理循环

默认情况下,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.JsonNewtonsoft.Json 一样,System.Text.Json.Serialization.ReferenceResolver 类定义在序列化和反序列化过程中保留引用的行为。 创建派生类以指定自定义行为。 有关示例,请参阅 GuidReferenceResolver

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

.NET Core 3.1 中的 System.Text.Json 仅支持按值进行进行序列化,并对循环引用引发异常。

包含非字符串键的字典

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

注意

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

Newtonsoft.Json 支持类型 Dictionary<TKey, TValue> 的集合。 .NET Core 3.1 的 System.Text.Json 中对字典集合的内置支持仅限于 Dictionary<string, TValue>。 即,键必须是字符串。

若要在 .NET Core 3.1 中支持将整数或某种其他类型用作键的字典,请创建转换器(类似于如何编写自定义转换器中的示例)。

没有内置支持的类型

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

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

多态序列化

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

多态反序列化

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

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

对象属性的反序列化

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

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

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

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

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

若要为 object 实现类型推理,请创建转换器(类似于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);
    }
}

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

注意:前面的转换器处理 null 值的方式与 为指定默认值的 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 中,使用 System.Text.Json 特性来指定参数化构造函数的用法。 C# 9 记录也是不可变的,并且支持作为反序列化目标。 有关详细信息,请参阅不可变类型和记录

.NET Core 3.1 中的 System.Text.Json 仅支持公共无参数构造函数。 作为一种解决方法,可以在自定义转换器中调用具有参数的构造函数。

下面是具有多个构造函数参数的不可变结构:

public readonly struct ImmutablePoint
{
    public ImmutablePoint(int x, int y)
    {
        X = x;
        Y = y;
    }

    public int X { get; }
    public int Y { get; }
}

下面是序列化和反序列化此结构的转换器:

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

namespace SystemTextJsonSamples
{
    public class ImmutablePointConverter : JsonConverter<ImmutablePoint>
    {
        private readonly JsonEncodedText _xName = JsonEncodedText.Encode("X");
        private readonly JsonEncodedText _yName = JsonEncodedText.Encode("Y");

        private readonly JsonConverter<int> _intConverter;

        public ImmutablePointConverter(JsonSerializerOptions options) => 
            _intConverter = options?.GetConverter(typeof(int)) is JsonConverter<int> intConverter
                ? intConverter
                : throw new InvalidOperationException();

        public override ImmutablePoint Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            };

            int? x = default;
            int? y = default;

            // Get the first property.
            reader.Read();
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException();
            }

            if (reader.ValueTextEquals(_xName.EncodedUtf8Bytes))
            {
                x = ReadProperty(ref reader, options);
            }
            else if (reader.ValueTextEquals(_yName.EncodedUtf8Bytes))
            {
                y = ReadProperty(ref reader, options);
            }
            else
            {
                throw new JsonException();
            }

            // Get the second property.
            reader.Read();
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException();
            }

            if (x.HasValue && reader.ValueTextEquals(_yName.EncodedUtf8Bytes))
            {
                y = ReadProperty(ref reader, options);
            }
            else if (y.HasValue && reader.ValueTextEquals(_xName.EncodedUtf8Bytes))
            {
                x = ReadProperty(ref reader, options);
            }
            else
            {
                throw new JsonException();
            }

            reader.Read();

            if (reader.TokenType != JsonTokenType.EndObject)
            {
                throw new JsonException();
            }

            return new ImmutablePoint(x.GetValueOrDefault(), y.GetValueOrDefault());
        }

        private int ReadProperty(ref Utf8JsonReader reader, JsonSerializerOptions options)
        {
            Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);

            reader.Read();
            return _intConverter.Read(ref reader, typeof(int), options);
        }

        private void WriteProperty(Utf8JsonWriter writer, JsonEncodedText name, int intValue, JsonSerializerOptions options)
        {
            writer.WritePropertyName(name);
            _intConverter.Write(writer, intValue, options);
        }

        public override void Write(
            Utf8JsonWriter writer,
            ImmutablePoint point,
            JsonSerializerOptions options)
        {
            writer.WriteStartObject();
            WriteProperty(writer, _xName, point.X, options);
            WriteProperty(writer, _yName, point.Y, options);
            writer.WriteEndObject();
        }
    }
}

通过向 集合添加转换器来注册此自定义转换器。

有关处理开放式泛型属性的类似转换器的示例,请参阅用于键/值对的内置转换器

必需的属性

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

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

如果没有为目标类型的某个属性收到值,System.Text.Json 不会引发异常。 例如,如果具有 WeatherForecast 类:

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

以下 JSON 可反序列化,不会发生错误:

{
    "TemperatureCelsius": 25,
    "Summary": "Hot"
}

若要使反序列化在 JSON 中没有 Date 属性时失败,请选择以下选项之一:

如果反序列化完成之后未设置 Date 属性,则以下示例转换器代码会引发异常:

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

namespace SystemTextJsonSamples
{
    public class WeatherForecastRequiredPropertyConverter : JsonConverter<WeatherForecast>
    {
        public override WeatherForecast Read(
            ref Utf8JsonReader reader,
            Type type,
            JsonSerializerOptions options)
        {
            // Don't pass in options when recursively calling Deserialize.
            WeatherForecast forecast = JsonSerializer.Deserialize<WeatherForecast>(ref reader)!;

            // Check for required fields set by values in JSON
            return forecast!.Date == default
                ? throw new JsonException("Required property not received in the JSON")
                : forecast;
        }

        public override void Write(
            Utf8JsonWriter writer,
            WeatherForecast forecast, JsonSerializerOptions options)
        {
            // Don't pass in options when recursively calling Serialize.
            JsonSerializer.Serialize(writer, forecast);
        }
    }
}

通过向 集合添加转换器来注册此自定义转换器。

这种以递归方式调用转换器的模式要求使用 JsonSerializerOptions 而不是使用属性注册转换器。 如果使用属性注册转换器,则自定义转换器将以递归方式调入其自身。 结果是一个以堆栈溢出异常结尾的无限循环。

使用选项对象注册转换器时,请通过在以递归方式调用 SerializeDeserialize 时不传入选项对象来避免无限循环。 选项对象包含 Converters 集合。 如果将它传递给 SerializeDeserialize,则自定义转换器会调入其自身,从而产生导致堆栈溢出异常的无限循环。 如果默认选项不可行,请使用所需设置创建选项的新实例。 此方法会速度较慢,因为每个新实例都会独立缓存。

有一种替代模式,可在要转换的类上使用 JsonConverterAttribute 注册。 在此方法中,转换器代码对派生自要转换的类的类调用 SerializeDeserialize。 派生类没有应用 JsonConverterAttribute。 在此替代的以下示例中:

  • WeatherForecastWithRequiredPropertyConverterAttribute 是要进行反序列化并应用 JsonConverterAttribute 的类。
  • WeatherForecastWithoutRequiredPropertyConverterAttribute 是不具有转换器属性的派生类。
  • 转换器中的代码调用 WeatherForecastWithoutRequiredPropertyConverterAttribute 上的 SerializeDeserialize 以避免无限循环。 此方法对于序列化是一种性能开销,因为需要实例化额外的对象和复制属性值。

WeatherForecast* 类型如下:

[JsonConverter(typeof(WeatherForecastRequiredPropertyConverterForAttributeRegistration))]
public class WeatherForecastWithRequiredPropertyConverterAttribute
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

public class WeatherForecastWithoutRequiredPropertyConverterAttribute :
    WeatherForecastWithRequiredPropertyConverterAttribute
{
}

下面是转换器:

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

namespace SystemTextJsonSamples
{
    public class WeatherForecastRequiredPropertyConverterForAttributeRegistration :
        JsonConverter<WeatherForecastWithRequiredPropertyConverterAttribute>
    {
        public override WeatherForecastWithRequiredPropertyConverterAttribute Read(
            ref Utf8JsonReader reader,
            Type type,
            JsonSerializerOptions options)
        {
            // OK to pass in options when recursively calling Deserialize.
            WeatherForecastWithRequiredPropertyConverterAttribute forecast =
                JsonSerializer.Deserialize<WeatherForecastWithoutRequiredPropertyConverterAttribute>(
                    ref reader,
                    options)!;

            // Check for required fields set by values in JSON.
            return forecast!.Date == default
                ? throw new JsonException("Required property not received in the JSON")
                : forecast;
        }

        public override void Write(
            Utf8JsonWriter writer,
            WeatherForecastWithRequiredPropertyConverterAttribute forecast,
            JsonSerializerOptions options)
        {
            var weatherForecastWithoutConverterAttributeOnClass =
                new WeatherForecastWithoutRequiredPropertyConverterAttribute
                {
                    Date = forecast.Date,
                    TemperatureCelsius = forecast.TemperatureCelsius,
                    Summary = forecast.Summary
                };

            // OK to pass in options when recursively calling Serialize.
            JsonSerializer.Serialize(
                writer,
                weatherForecastWithoutConverterAttributeOnClass,
                options);
        }
    }
}

如果需要处理特性(例如 [JsonIgnore])或不同选项(如自定义编码器),必需的属性转换器需要其他逻辑。 此外,示例代码不处理在构造函数中为其设置了默认值的属性。 而且此方法不区分以下情况:

  • JSON 中缺少属性。
  • JSON 中存在不可为 null 的类型的属性,但值是该类型的默认值,如 int 的值为零。
  • JSON 中存在可为 null 的值类型的属性,但值为 null。

注意

如果从 ASP.NET Core 控制器使用 System.Text.Json,则也许能够在模型类的属性上使用 [Required] 特性,而不是实现 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 DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
    static readonly Regex s_regex = new Regex("^/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 TimeSpan(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 = FormattableString.Invariant($"/Date({unixTime}{(utcOffset >= TimeSpan.Zero ? "+" : "-")}{utcOffset:hhmm})/");
        writer.WriteStringValue(formatted);
    }
}
sealed class UnixEpochDateTimeConverter : JsonConverter<DateTime>
{
    static readonly DateTime s_epoch = new DateTime(1970, 1, 1, 0, 0, 0);
    static readonly Regex s_regex = new Regex("^/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 = FormattableString.Invariant($"/Date({unixTime})/");
        writer.WriteStringValue(formatted);
    }
}

有关详细信息,请参阅 中的 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 构造函数中。

System.Text.Json 中,可以通过编写自定义转换器来模拟回调。 以下示例演示适用于 POCO 的自定义转换器。 该转换器包含在与 Newtonsoft.Json 回调相对应的每个点显示消息的代码。

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

namespace SystemTextJsonSamples
{
    public class WeatherForecastCallbacksConverter : JsonConverter<WeatherForecast>
    {
        public override WeatherForecast Read(
            ref Utf8JsonReader reader,
            Type type,
            JsonSerializerOptions options)
        {
            // Place "before" code here (OnDeserializing),
            // but note that there is no access here to the POCO instance.
            Console.WriteLine("OnDeserializing");

            // Don't pass in options when recursively calling Deserialize.
            WeatherForecast forecast = JsonSerializer.Deserialize<WeatherForecast>(ref reader)!;

            // Place "after" code here (OnDeserialized)
            Console.WriteLine("OnDeserialized");

            return forecast;
        }

        public override void Write(
            Utf8JsonWriter writer,
            WeatherForecast forecast, JsonSerializerOptions options)
        {
            // Place "before" code here (OnSerializing)
            Console.WriteLine("OnSerializing");

            // Don't pass in options when recursively calling Serialize.
            JsonSerializer.Serialize(writer, forecast);

            // Place "after" code here (OnSerialized)
            Console.WriteLine("OnSerialized");
        }
    }
}

通过向 集合添加转换器来注册此自定义转换器。

如果使用遵循前面示例的自定义转换器:

  • OnDeserializing 代码无权访问新 POCO 实例。 若要在反序列化开始时操作新 POCO 实例,请将该代码放入 POCO 构造函数中。
  • 通过在选项对象中注册转换器而在以递归方式调用 SerializeDeserialize 时不传入选项对象,避免无限循环。

若要详细了解递归调用 SerializeDeserialize 的自定义转换器,请参阅本文前面的Serialize部分。

非公共属性资源库和 Getter

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

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

.NET Core 3.1 中的 System.Text.Json 仅支持公共资源库。 自定义转换器可提供此功能。

填充现有对象

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

重用而不是替换属性

通过 Newtonsoft.JsonObjectCreationHandling 设置,可指定在反序列化过程中应重用属性中的对象,而不是进行替换。 System.Text.Json 始终替换属性中的对象。 自定义转换器可提供此功能。

在不带 setter 的情况下添加到集合

在反序列化过程中,Newtonsoft.Json 会将对象添加到集合,即使属性没有 setter。 System.Text.Json 会忽略没有 setter 的属性。 自定义转换器可提供此功能。

蛇形命名法命名策略

System.Text.Json 中唯一的内置属性命名策略适用于System.Text.Json。 Newtonsoft.Json 可将属性名称转换为蛇形命名法。 自定义命名策略可提供此功能。 有关详细信息,请参阅 GitHub 问题 dotnet/runtime #782

System.Runtime.Serialization 特性

System.Text.Json 不支持 System.Runtime.Serialization 命名空间中的特性,如 DataMemberAttributeIgnoreDataMemberAttribute

八进制数字

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

MissingMemberHandling

Newtonsoft.Json 可以配置为在 JSON 包含目标类型中缺少的属性时,在反序列化过程中引发异常。 System.Text.Json 会忽略 JSON 中的额外属性,但在使用 System.Text.Json时除外。 对于缺少成员功能,没有解决方法。

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 命名空间中的 类型和其他类型,从现有 JSON 有效负载分析和生成可变 DOM。 有关详细信息,请参阅使用

JsonDocument 为 IDisposable

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

JsonDocument 为只读

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

如果你的方案当前使用可修改的 DOM,则以下解决方法之一可能是可行的:

  • 若要从头开始生成 JsonDocument(即,不将现有 JSON 有效负载传入到 Parse 方法),请使用 Utf8JsonWriter 编写 JSON 文本,并分析这样做的输出以创建新 JsonDocument
  • 若要修改现有 JsonDocument,请使用它编写 JSON 文本(在编写时进行更改),并分析这样做的输出以创建新 JsonDocument
  • 若要合并现有 JSON 文档(与 Newtonsoft.Json 中的 JObject.MergeJContainer.Merge API 等效),请参阅JObject.Merge

只有低于 6.0 的 System.Text.Json 版本才需要这些解决方法。 在 6.0 版中,可使用 JsonNode 来处理可变 DOM。

JsonElement 是联合结构

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

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

如何搜索子元素的 JsonDocument 和 JsonElement

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

Utf8JsonReader 与 JsonTextReader 的比较

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

Utf8JsonReader 是 ref struct

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

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

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

多目标

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

Utf8JsonWriter 与 JsonTextWriter 的比较

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

编写原始值

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

Newtonsoft.JsonWriteRawValue 方法可编写原始 JSON(其中需要值)。 在 .NET 6 及更高版本中,有一个等效的方法,Utf8JsonWriter.WriteRawValue。 有关详细信息,请参阅写入原始 JSON

对于 6.0 之前的版本,System.Text.Json 没有用于写入原始 JSON 的等效方法。 但是,以下解决方法可确保仅编写有效的 JSON:

using JsonDocument doc = JsonDocument.Parse(string);
doc.WriteTo(writer);

自定义 JSON 格式

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

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

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

编写 Timespan、Uri 或 char 值

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

多目标

如果需要继续为某些目标框架使用 Newtonsoft.Json,则可以使用多目标,并具有两种实现。 但是,这并非易事,需要进行一些 #ifdefs 和源文件复制。 共享尽可能多代码的一种方法是围绕 Utf8JsonWriterNewtonsoftJsonTextWriter 创建包装器。 此包装器会统一公共外围应用,同时隔离行为差异。 这使你可以隔离主要对类型的构造进行的更改。 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 不支持使用 JsonDocument进行查询。

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

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

某些限制不可配置

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

NaN、Infinity、-Infinity

Newtonsoft 会分析 NaNInfinity-Infinity JSON 字符串标记。 在 .NET Core 3.1 中,System.Text.Json 不支持这些标记,但你可编写自定义转换器来处理它们。 在 .NET 5 及更高版本中,请使用 JsonNumberHandling.AllowNamedFloatingPointLiterals。 若要了解如何使用此设置,请参阅允许或写入引号中的数字

其他资源