如何在 System.Text.Json 中使用 JSON 文档、Utf8JsonReader 和 Utf8JsonWriter

本文介绍如何使用:

JSON DOM 选项

在以下情况中,可以不使用 JsonSerializer 进行反序列化,而是使用 DOM:

  • 当没有要反序列化到的类型时。
  • 当收到的 JSON 没有固定架构,并且必须检查了解它包含哪些内容时。

System.Text.Json 提供了两种方法来生成 JSON DOM:

  • 通过 JsonDocument,可使用 Utf8JsonReader 生成只读 DOM。 可以通过 JsonElement 类型访问构成有效负载的 JSON 元素。 JsonElement 类型提供数组和对象枚举器,以及用于将 JSON 文本转换为常见 .NET 类型的 API。 JsonDocument 公开了 RootElement 属性。 有关详细信息,请参阅本文后面的使用 JsonDocument

JsonDocumentJsonNode 之间进行选择时,请考虑以下因素:

  • 创建 JsonNode DOM 后,可更改它。 JsonDocument DOM 是不可变的。
  • 通过 JsonDocument DOM 可更快地访问其数据。

使用 JsonNode

以下示例演示如何使用 JsonNode 以及 System.Text.Json.Nodes 命名空间中的其他类型来执行以下操作:

  • 从 JSON 字符串创建 DOM
  • 从 DOM 写入 JSON。
  • 从 DOM 获取值、对象或数组。
using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodeFromStringExample;

public class Program
{
    public static void Main()
    {
        string jsonString =
@"{
  ""Date"": ""2019-08-01T00:00:00"",
  ""Temperature"": 25,
  ""Summary"": ""Hot"",
  ""DatesAvailable"": [
    ""2019-08-01T00:00:00"",
    ""2019-08-02T00:00:00""
  ],
  ""TemperatureRanges"": {
      ""Cold"": {
          ""High"": 20,
          ""Low"": -10
      },
      ""Hot"": {
          ""High"": 60,
          ""Low"": 20
      }
  }
}
";
        // Create a JsonNode DOM from a JSON string.
        JsonNode forecastNode = JsonNode.Parse(jsonString)!;

        // Write JSON from a JsonNode
        var options = new JsonSerializerOptions { WriteIndented = true };
        Console.WriteLine(forecastNode!.ToJsonString(options));
        // output:
        //{
        //  "Date": "2019-08-01T00:00:00",
        //  "Temperature": 25,
        //  "Summary": "Hot",
        //  "DatesAvailable": [
        //    "2019-08-01T00:00:00",
        //    "2019-08-02T00:00:00"
        //  ],
        //  "TemperatureRanges": {
        //    "Cold": {
        //      "High": 20,
        //      "Low": -10
        //    },
        //    "Hot": {
        //      "High": 60,
        //      "Low": 20
        //    }
        //  }
        //}

        // Get value from a JsonNode.
        JsonNode temperatureNode = forecastNode!["Temperature"]!;
        Console.WriteLine($"Type={temperatureNode.GetType()}");
        Console.WriteLine($"JSON={temperatureNode.ToJsonString()}");
        //output:
        //Type = System.Text.Json.Nodes.JsonValue`1[System.Text.Json.JsonElement]
        //JSON = 25

        // Get a typed value from a JsonNode.
        int temperatureInt = (int)forecastNode!["Temperature"]!;
        Console.WriteLine($"Value={temperatureInt}");
        //output:
        //Value=25

        // Get a typed value from a JsonNode by using GetValue<T>.
        temperatureInt = forecastNode!["Temperature"]!.GetValue<int>();
        Console.WriteLine($"TemperatureInt={temperatureInt}");
        //output:
        //Value=25

        // Get a JSON object from a JsonNode.
        JsonNode temperatureRanges = forecastNode!["TemperatureRanges"]!;
        Console.WriteLine($"Type={temperatureRanges.GetType()}");
        Console.WriteLine($"JSON={temperatureRanges.ToJsonString()}");
        //output:
        //Type = System.Text.Json.Nodes.JsonObject
        //JSON = { "Cold":{ "High":20,"Low":-10},"Hot":{ "High":60,"Low":20} }

        // Get a JSON array from a JsonNode.
        JsonNode datesAvailable = forecastNode!["DatesAvailable"]!;
        Console.WriteLine($"Type={datesAvailable.GetType()}");
        Console.WriteLine($"JSON={datesAvailable.ToJsonString()}");
        //output:
        //datesAvailable Type = System.Text.Json.Nodes.JsonArray
        //datesAvailable JSON =["2019-08-01T00:00:00", "2019-08-02T00:00:00"]

        // Get an array element value from a JsonArray.
        JsonNode firstDateAvailable = datesAvailable[0]!;
        Console.WriteLine($"Type={firstDateAvailable.GetType()}");
        Console.WriteLine($"JSON={firstDateAvailable.ToJsonString()}");
        //output:
        //Type = System.Text.Json.Nodes.JsonValue`1[System.Text.Json.JsonElement]
        //JSON = "2019-08-01T00:00:00"

        // Get a typed value by chaining references.
        int coldHighTemperature = (int)forecastNode["TemperatureRanges"]!["Cold"]!["High"]!;
        Console.WriteLine($"TemperatureRanges.Cold.High={coldHighTemperature}");
        //output:
        //TemperatureRanges.Cold.High = 20

        // Parse a JSON array
        var datesNode = JsonNode.Parse(@"[""2019-08-01T00:00:00"",""2019-08-02T00:00:00""]");
        JsonNode firstDate = datesNode![0]!.GetValue<DateTime>();
        Console.WriteLine($"firstDate={ firstDate}");
        //output:
        //firstDate = "2019-08-01T00:00:00"
    }
}

使用对象初始值设定项创建 JsonNode DOM 并进行更改

以下示例介绍如何:

  • 使用对象初始值设定项创建 DOM。
  • 对 DOM 进行更改。
using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodeFromObjectExample;

public class Program
{
    public static void Main()
    {
        // Create a new JsonObject using object initializers.
        var forecastObject = new JsonObject
        {
            ["Date"] = new DateTime(2019, 8, 1),
            ["Temperature"] = 25,
            ["Summary"] = "Hot",
            ["DatesAvailable"] = new JsonArray(
                new DateTime(2019, 8, 1), new DateTime(2019, 8, 2)),
            ["TemperatureRanges"] = new JsonObject
            {
                ["Cold"] = new JsonObject
                {
                    ["High"] = 20,
                    ["Low"] = -10
                }
            },
            ["SummaryWords"] = new JsonArray("Cool", "Windy", "Humid")
        };

        // Add an object.
        forecastObject!["TemperatureRanges"]!["Hot"] =
            new JsonObject { ["High"] = 60, ["Low"] = 20 };

        // Remove a property.
        forecastObject.Remove("SummaryWords");

        // Change the value of a property.
        forecastObject["Date"] = new DateTime(2019, 8, 3);

        var options = new JsonSerializerOptions { WriteIndented = true };
        Console.WriteLine(forecastObject.ToJsonString(options));
        //output:
        //{
        //  "Date": "2019-08-03T00:00:00",
        //  "Temperature": 25,
        //  "Summary": "Hot",
        //  "DatesAvailable": [
        //    "2019-08-01T00:00:00",
        //    "2019-08-02T00:00:00"
        //  ],
        //  "TemperatureRanges": {
        //    "Cold": {
        //      "High": 20,
        //      "Low": -10
        //    },
        //    "Hot": {
        //      "High": 60,
        //      "Low": 20
        //    }
        //  }
        //}
    }
}

对 JSON 有效负载的子节进行反初始化

以下示例演示如何使用 JsonNode 导航到 JSON 树的子节,以及如何反序列化该子节中的单个值、自定义类型或数组。

using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodePOCOExample;

public class TemperatureRanges : Dictionary<string, HighLowTemps>
{
}

public class HighLowTemps
{
    public int High { get; set; }
    public int Low { get; set; }
}

public class Program
{
    public static DateTime[]? DatesAvailable { get; set; }

    public static void Main()
    {
        string jsonString =
@"{
  ""Date"": ""2019-08-01T00:00:00"",
  ""Temperature"": 25,
  ""Summary"": ""Hot"",
  ""DatesAvailable"": [
    ""2019-08-01T00:00:00"",
    ""2019-08-02T00:00:00""
  ],
  ""TemperatureRanges"": {
      ""Cold"": {
          ""High"": 20,
          ""Low"": -10
      },
      ""Hot"": {
          ""High"": 60,
          ""Low"": 20
      }
  }
}
";
        // Parse all of the JSON.
        JsonNode forecastNode = JsonNode.Parse(jsonString)!;

        // Get a single value
        int hotHigh = forecastNode["TemperatureRanges"]!["Hot"]!["High"]!.GetValue<int>();
        Console.WriteLine($"Hot.High={hotHigh}");
        // output:
        //Hot.High=60

        // Get a subsection and deserialize it into a custom type.
        JsonObject temperatureRangesObject = forecastNode!["TemperatureRanges"]!.AsObject();
        using var stream = new MemoryStream();
        using var writer = new Utf8JsonWriter(stream);
        temperatureRangesObject.WriteTo(writer);
        writer.Flush();
        TemperatureRanges? temperatureRanges = 
            JsonSerializer.Deserialize<TemperatureRanges>(stream.ToArray());
        Console.WriteLine($"Cold.Low={temperatureRanges!["Cold"].Low}, Hot.High={temperatureRanges["Hot"].High}");
        // output:
        //Cold.Low=-10, Hot.High=60

        // Get a subsection and deserialize it into an array.
        JsonArray datesAvailable = forecastNode!["DatesAvailable"]!.AsArray()!;
        Console.WriteLine($"DatesAvailable[0]={datesAvailable[0]}");
        // output:
        //DatesAvailable[0]=8/1/2019 12:00:00 AM
    }
}

JsonNode 平均等级示例

以下示例选择一个包含整数值的 JSON 数组,并计算平均值:

using System.Text.Json.Nodes;

namespace JsonNodeAverageGradeExample;

public class Program
{
    public static void Main()
    {
        string jsonString =
@"{
  ""Class Name"": ""Science"",
  ""Teacher\u0027s Name"": ""Jane"",
  ""Semester"": ""2019-01-01"",
  ""Students"": [
    {
      ""Name"": ""John"",
      ""Grade"": 94.3
    },
    {
      ""Name"": ""James"",
      ""Grade"": 81.0
    },
    {
      ""Name"": ""Julia"",
      ""Grade"": 91.9
    },
    {
      ""Name"": ""Jessica"",
      ""Grade"": 72.4
    },
    {
      ""Name"": ""Johnathan""
    }
  ],
  ""Final"": true
}
";
        double sum = 0;
        int count = 0;

        JsonNode document = JsonNode.Parse(jsonString)!;

        JsonNode root = document.Root;
        JsonArray studentsArray = root["Students"]!.AsArray();

        count = studentsArray.Count;

        foreach (JsonNode? student in studentsArray)
        {
            if (student?["Grade"] is JsonNode gradeNode)
            {
                sum += (double)gradeNode;
            }
            else
            {
                sum += 70;
            }
        }

        double average = sum / count;
        Console.WriteLine($"Average grade : {average}");
    }
}
// output:
//Average grade : 81.92

前面的代码:

  • Students 数组中具有 Grade 属性的对象计算平均成绩。
  • 为没有成绩的学生分配默认成绩 70。
  • JsonArrayCount 属性获取学生人数。

JsonSerializerOptionsJsonNode

可以使用 JsonSerializerJsonNode 的实例进行序列化和反序列化。 但是,如果使用采用 JsonSerializerOptions 的重载,则 options 实例仅用于获取自定义转换器。 不使用 options 实例的其他功能。 例如,如果将 JsonSerializerOptions.DefaultIgnoreCondition 设置为 WhenWritingNull,并且使用采用 JsonSerializerOptions 的重载来调用 JsonSerializer,则不会忽略 NULL 属性。

相同的限制适用于采用 JsonSerializerOptions 参数的 JsonNode 方法:WriteTo(Utf8JsonWriter, JsonSerializerOptions)ToJsonString(JsonSerializerOptions)。 这些 API 仅使用 JsonSerializerOptions 获取自定义转换器。

下面的示例演示了使用采用 JsonSerializerOptions 参数并对 JsonNode 实例进行序列化的方法的结果:

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

namespace JsonNodeWithJsonSerializerOptions;

public class Program
{
    public static void Main()
    {
        Person person = new Person { Name = "Nancy" };

        // Default serialization - Address property included with null token.
        // Output: {"Name":"Nancy","Address":null}
        string personJsonWithNull = JsonSerializer.Serialize(person);
        Console.WriteLine(personJsonWithNull);

        // Serialize and ignore null properties - null Address property is omitted
        // Output: {"Name":"Nancy"}
        JsonSerializerOptions options = new()
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
        string personJsonWithoutNull = JsonSerializer.Serialize(person, options);
        Console.WriteLine(personJsonWithoutNull);

        // Ignore null properties doesn't work when serializing JsonNode instance
        // by using JsonSerializer.
        // Output: {"Name":"Nancy","Address":null}
        var personJsonNode = JsonSerializer.Deserialize<JsonNode>(personJsonWithNull);
        personJsonWithNull = JsonSerializer.Serialize(personJsonNode, options);
        Console.WriteLine(personJsonWithNull);

        // Ignore null properties doesn't work when serializing JsonNode instance
        // by using JsonNode.ToJsonString method.
        // Output: {"Name":"Nancy","Address":null}
        personJsonWithNull = personJsonNode!.ToJsonString(options);
        Console.WriteLine(personJsonWithNull);

        // Ignore null properties doesn't work when serializing JsonNode instance
        // by using JsonNode.WriteTo method.
        // Output: {"Name":"Nancy","Address":null}
        using var stream = new MemoryStream();
        using var writer = new Utf8JsonWriter(stream);
        personJsonNode!.WriteTo(writer, options);
        writer.Flush();
        personJsonWithNull = Encoding.UTF8.GetString(stream.ToArray());
        Console.WriteLine(personJsonWithNull);
    }
}
public class Person
{
    public string? Name { get; set; }
    public string? Address { get; set; }
}

如果需要 JsonSerializerOptions 除自定义转换器之外的功能,请将 JsonSerializer 与强类型目标(如本示例中的 Person 类)而不是 JsonNode 结合使用。

使用 JsonDocument

下面的示例演示如何使用 JsonDocument 类来随机访问 JSON 字符串中的数据:

double sum = 0;
int count = 0;

using (JsonDocument document = JsonDocument.Parse(jsonString))
{
    JsonElement root = document.RootElement;
    JsonElement studentsElement = root.GetProperty("Students");
    foreach (JsonElement student in studentsElement.EnumerateArray())
    {
        if (student.TryGetProperty("Grade", out JsonElement gradeElement))
        {
            sum += gradeElement.GetDouble();
        }
        else
        {
            sum += 70;
        }
        count++;
    }
}

double average = sum / count;
Console.WriteLine($"Average grade : {average}");
Dim sum As Double = 0
Dim count As Integer = 0
Using document As JsonDocument = JsonDocument.Parse(jsonString)
    Dim root As JsonElement = document.RootElement
    Dim studentsElement As JsonElement = root.GetProperty("Students")
    For Each student As JsonElement In studentsElement.EnumerateArray()
        Dim gradeElement As JsonElement = Nothing
        If student.TryGetProperty("Grade", gradeElement) Then
            sum += gradeElement.GetDouble()
        Else
            sum += 70
        End If
        count += 1
    Next
End Using

Dim average As Double = sum / count
Console.WriteLine($"Average grade : {average}")

前面的代码:

  • 假设要分析的 JSON 处于名为 jsonString 的字符串中。
  • Students 数组中具有 Grade 属性的对象计算平均成绩。
  • 为没有成绩的学生分配默认成绩 70。
  • using 语句中创建 JsonDocument 实例,因为 JsonDocument 会实现 IDisposable。 释放 JsonDocument 实例后,你也无法在访问其所有的 JsonElement 实例。 若要能够继续访问 JsonElement 实例,请创建该实例的副本,然后再释放父 JsonDocument 实例。 若要创建副本,请调用 JsonElement.Clone。 有关详细信息,请参阅 JsonDocument 是 IDisposable

上述示例代码通过每次迭代递增 count 变量来计算学生人数。 一种替代方法是调用 GetArrayLength,如以下示例中所示:

double sum = 0;
int count = 0;

using (JsonDocument document = JsonDocument.Parse(jsonString))
{
    JsonElement root = document.RootElement;
    JsonElement studentsElement = root.GetProperty("Students");

    count = studentsElement.GetArrayLength();

    foreach (JsonElement student in studentsElement.EnumerateArray())
    {
        if (student.TryGetProperty("Grade", out JsonElement gradeElement))
        {
            sum += gradeElement.GetDouble();
        }
        else
        {
            sum += 70;
        }
    }
}

double average = sum / count;
Console.WriteLine($"Average grade : {average}");
Dim sum As Double = 0
Dim count As Integer = 0
Using document As JsonDocument = JsonDocument.Parse(jsonString)
    Dim root As JsonElement = document.RootElement
    Dim studentsElement As JsonElement = root.GetProperty("Students")

    count = studentsElement.GetArrayLength()

    For Each student As JsonElement In studentsElement.EnumerateArray()
        Dim gradeElement As JsonElement = Nothing
        If student.TryGetProperty("Grade", gradeElement) Then
            sum += gradeElement.GetDouble()
        Else
            sum += 70
        End If
    Next
End Using

Dim average As Double = sum / count
Console.WriteLine($"Average grade : {average}")

下面是此代码处理的 JSON 示例:

{
  "Class Name": "Science",
  "Teacher\u0027s Name": "Jane",
  "Semester": "2019-01-01",
  "Students": [
    {
      "Name": "John",
      "Grade": 94.3
    },
    {
      "Name": "James",
      "Grade": 81.0
    },
    {
      "Name": "Julia",
      "Grade": 91.9
    },
    {
      "Name": "Jessica",
      "Grade": 72.4
    },
    {
      "Name": "Johnathan"
    }
  ],
  "Final": true
}

如需使用 JsonNode 而不是 JsonDocument 的类似示例,请参阅 JsonNode 平均等级示例

如何搜索子元素的 JsonDocument 和 JsonElement

若要对 JsonElement 进行搜索,需要对属性进行线性搜索,因此速度相对较慢(例如在使用 TryGetProperty 时)。 System.Text.Json 旨在最大程度减少初始分析时间,而不是查找时间。 因此,在通过 JsonDocument 对象搜索时,请使用以下方法优化性能:

  • 使用内置枚举器(EnumerateArrayEnumerateObject),而不是执行自己的索引或循环。
  • 不要使用 RootElement 通过每个属性对整个 JsonDocument 执行线性搜索。 而是基于 JSON 数据的已知结构对嵌套 JSON 对象进行搜索。 例如,前面的代码示例在 Student 对象中查找 Grade 属性,方法是循环访问 Student 对象,并获取每个对象的 Grade 值,而不是搜索所有 Grade 对象来查找 JsonElement 属性。 执行后者会导致不必要浏览相同数据。

使用 JsonDocument 编写 JSON

下面的示例演示如何通过 JsonDocument 写入 JSON:

string jsonString = File.ReadAllText(inputFileName);

var writerOptions = new JsonWriterOptions
{
    Indented = true
};

var documentOptions = new JsonDocumentOptions
{
    CommentHandling = JsonCommentHandling.Skip
};

using FileStream fs = File.Create(outputFileName);
using var writer = new Utf8JsonWriter(fs, options: writerOptions);
using JsonDocument document = JsonDocument.Parse(jsonString, documentOptions);

JsonElement root = document.RootElement;

if (root.ValueKind == JsonValueKind.Object)
{
    writer.WriteStartObject();
}
else
{
    return;
}

foreach (JsonProperty property in root.EnumerateObject())
{
    property.WriteTo(writer);
}

writer.WriteEndObject();

writer.Flush();
Dim jsonString As String = File.ReadAllText(inputFileName)

Dim writerOptions As JsonWriterOptions = New JsonWriterOptions With {
    .Indented = True
}

Dim documentOptions As JsonDocumentOptions = New JsonDocumentOptions With {
    .CommentHandling = JsonCommentHandling.Skip
}

Dim fs As FileStream = File.Create(outputFileName)
Dim writer As Utf8JsonWriter = New Utf8JsonWriter(fs, options:=writerOptions)
Dim document As JsonDocument = JsonDocument.Parse(jsonString, documentOptions)

Dim root As JsonElement = document.RootElement

If root.ValueKind = JsonValueKind.[Object] Then
    writer.WriteStartObject()
Else
    Return
End If

For Each [property] As JsonProperty In root.EnumerateObject()
    [property].WriteTo(writer)
Next

writer.WriteEndObject()

writer.Flush()

前面的代码:

  • 读取 JSON 文件,将数据加载到 JsonDocument 中,并将格式化(进行了优质打印的)JSON 写入文件。
  • 使用 JsonDocumentOptions 可指定允许但会忽略输入 JSON 中的注释。
  • 完成后,对编写器调用 Flush。 一种替代方法是让编写器在释放时自动刷新。

下面是要由示例代码处理的 JSON 输入的示例:

{"Class Name": "Science","Teacher's Name": "Jane","Semester": "2019-01-01","Students": [{"Name": "John","Grade": 94.3},{"Name": "James","Grade": 81.0},{"Name": "Julia","Grade": 91.9},{"Name": "Jessica","Grade": 72.4},{"Name": "Johnathan"}],"Final": true}

结果是以下进行了优质打印的 JSON 输出:

{
  "Class Name": "Science",
  "Teacher\u0027s Name": "Jane",
  "Semester": "2019-01-01",
  "Students": [
    {
      "Name": "John",
      "Grade": 94.3
    },
    {
      "Name": "James",
      "Grade": 81.0
    },
    {
      "Name": "Julia",
      "Grade": 91.9
    },
    {
      "Name": "Jessica",
      "Grade": 72.4
    },
    {
      "Name": "Johnathan"
    }
  ],
  "Final": true
}

JsonDocument 为 IDisposable

JsonDocument 将内存中的数据视图生成到共用缓冲区中。 因此,JsonDocument 类型会实现 IDisposable,需要在 using 块内使用。

如果要将生存期所有权和释放责任转移到调用方,则只需从 API 返回 JsonDocument。 在大多数情况下,这不是必需的。 如果调用方需要处理整个 JSON 文档,则返回 RootElementClone,这是 JsonElement。 如果调用方需要处理 JSON 文档中的特定元素,则返回该 JsonElementClone。 如果在不进行 Clone 的情况下直接返回 RootElement 或子元素,则在释放拥有返回的 JsonElementJsonDocument 之后,调用方将无法访问它。

下面是一个要求你进行 Clone 的示例:

public JsonElement LookAndLoad(JsonElement source)
{
    string json = File.ReadAllText(source.GetProperty("fileName").GetString());

    using (JsonDocument doc = JsonDocument.Parse(json))
    {
        return doc.RootElement.Clone();
    }
}

前面的代码需要包含 fileName 属性的 JsonElement。 它会打开 JSON 文件并创建一个 JsonDocument。 该方法假设调用方要处理整个文档,因此会返回 RootElementClone

如果收到 JsonElement 并要返回子元素,则无需返回子元素的 Clone。 调用方负责使传入的 JsonElement 所属的 JsonDocument 保持活动状态。 例如:

public JsonElement ReturnFileName(JsonElement source)
{
   return source.GetProperty("fileName");
}

JsonSerializerOptionsJsonDocument

可以使用 JsonSerializerJsonDocument 的实例进行序列化和反序列化。 但是,使用 JsonSerializer 读取和写入 JsonDocument 实例的实现是 JsonDocument.ParseValue(Utf8JsonReader)JsonDocument.WriteTo(Utf8JsonWriter) 的包装器。 此包装器不会将任何 JsonSerializerOptions(序列化程序功能)转发到 Utf8JsonReaderUtf8JsonWriter。 例如,如果将 JsonSerializerOptions.DefaultIgnoreCondition 设置为 WhenWritingNull,并且使用采用 JsonSerializerOptions 的重载来调用 JsonSerializer,则不会忽略 NULL 属性。

下面的示例演示了使用采用 JsonSerializerOptions 参数并对 JsonDocument 实例进行序列化的方法的结果:

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

namespace JsonDocumentWithJsonSerializerOptions;

public class Program
{
    public static void Main()
    {
        Person person = new Person { Name = "Nancy" };

        // Default serialization - Address property included with null token.
        // Output: {"Name":"Nancy","Address":null}
        string personJsonWithNull = JsonSerializer.Serialize(person);
        Console.WriteLine(personJsonWithNull);

        // Serialize and ignore null properties - null Address property is omitted
        // Output: {"Name":"Nancy"}
        JsonSerializerOptions options = new()
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
        string personJsonWithoutNull = JsonSerializer.Serialize(person, options);
        Console.WriteLine(personJsonWithoutNull);

        // Ignore null properties doesn't work when serializing JsonDocument instance
        // by using JsonSerializer.
        // Output: {"Name":"Nancy","Address":null}
        var personJsonDocument = JsonSerializer.Deserialize<JsonDocument>(personJsonWithNull);
        personJsonWithNull = JsonSerializer.Serialize(personJsonDocument, options);
        Console.WriteLine(personJsonWithNull);
    }
}
public class Person
{
    public string? Name { get; set; }

    public string? Address { get; set; }
}

如果需要 JsonSerializerOptions 的功能,请将 JsonSerializer 与强类型目标(如本示例中的 Person 类)而不是 JsonDocument 结合使用。

使用 Utf8JsonWriter

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

下面的示例演示如何使用 Utf8JsonWriter 类:

var options = new JsonWriterOptions
{
    Indented = true
};

using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, options);

writer.WriteStartObject();
writer.WriteString("date", DateTimeOffset.UtcNow);
writer.WriteNumber("temp", 42);
writer.WriteEndObject();
writer.Flush();

string json = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(json);
Dim options As JsonWriterOptions = New JsonWriterOptions With {
    .Indented = True
}

Dim stream As MemoryStream = New MemoryStream
Dim writer As Utf8JsonWriter = New Utf8JsonWriter(stream, options)

writer.WriteStartObject()
writer.WriteString("date", DateTimeOffset.UtcNow)
writer.WriteNumber("temp", 42)
writer.WriteEndObject()
writer.Flush()

Dim json As String = Encoding.UTF8.GetString(stream.ToArray())
Console.WriteLine(json)

使用 UTF-8 文本进行编写

若要在使用 Utf8JsonWriter 时实现可能的最佳性能,请编写已编码为 UTF-8 文本(而不是 UTF-16 字符串)的 JSON 有效负载。 使用 JsonEncodedText 可缓存已知字符串属性名称和值并预先编码为静态,并将这些内容传递给编写器,而不是使用 UTF-16 字符串文本。 这比缓存并使用 UTF-8 字节数组更快。

如果需要进行自定义转义,此方法也适用。 System.Text.Json 不允许在编写字符串时禁用转义。 但是,可以将自己的自定义 JavaScriptEncoder 作为一个选项传入编写器,或创建自己的 JsonEncodedText 以使用你的 JavascriptEncoder 进行转义,然后编写 JsonEncodedText 而不是字符串。 有关详细信息,请参阅自定义字符编码

写入原始 JSON

在某些情况下,可能需要将“原始”JSON 写入使用 Utf8JsonWriter 创建的 JSON 有效负载。 可使用 Utf8JsonWriter.WriteRawValue 执行此操作。 下面是典型场景:

  • 你有一个要包含在新 JSON 中的现有 JSON 有效负载。

  • 你想要将值的格式设置与默认格式 Utf8JsonWriter 不同。

    例如,你可能想要自定义数字格式设置。 默认情况下,System.Text.Json 会省略整数的小数点,例如写入 1 而不是 1.0。 基本原理是编写更少的字节会提高性能。 但是,假设 JSON 的使用者将带小数的数字视为双精度值,将不带小数的数字视为整数。 你可能希望通过写入整数的小数部分和零,确保数组中的数字全部被识别为双精度值。 下面的示例演示如何执行此操作:

    using System.Text;
    using System.Text.Json;
    
    namespace WriteRawJson;
    
    public class Program
    {
        public static void Main()
        {
            JsonWriterOptions writerOptions = new() { Indented = true, };
    
            using MemoryStream stream = new();
            using Utf8JsonWriter writer = new(stream, writerOptions);
    
            writer.WriteStartObject();
    
            writer.WriteStartArray("defaultJsonFormatting");
            foreach (double number in new double[] { 50.4, 51 })
            {
                writer.WriteStartObject();
                writer.WritePropertyName("value");
                writer.WriteNumberValue(number);
                writer.WriteEndObject();
            }
            writer.WriteEndArray();
    
            writer.WriteStartArray("customJsonFormatting");
            foreach (double result in new double[] { 50.4, 51 })
            {
                writer.WriteStartObject();
                writer.WritePropertyName("value");
                writer.WriteRawValue(
                    FormatNumberValue(result), skipInputValidation: true);
                writer.WriteEndObject();
            }
            writer.WriteEndArray();
    
            writer.WriteEndObject();
            writer.Flush();
    
            string json = Encoding.UTF8.GetString(stream.ToArray());
            Console.WriteLine(json);
        }
        static string FormatNumberValue(double numberValue)
        {
            return numberValue == Convert.ToInt32(numberValue) ? 
                numberValue.ToString() + ".0" : numberValue.ToString();
        }
    }
    // output:
    //{
    //  "defaultJsonFormatting": [
    //    {
    //      "value": 50.4
    //    },
    //    {
    //      "value": 51
    //    }
    //  ],
    //  "customJsonFormatting": [
    //    {
    //      "value": 50.4
    //    },
    //    {
    //      "value": 51.0
    //    }
    //  ]
    //}
    

自定义字符转义

JsonTextWriterStringEscapeHandling 设置提供用于转移所有非 ASCII 字符或 HTML 字符的选项。 默认情况下,Utf8JsonWriter 会转义所有非 ASCII 和 HTML 字符。 进行此转义是出于深度防御安全原因。 若要指定不同的转义策略,请创建 JavaScriptEncoder 并设置 JsonWriterOptions.Encoder。 有关详细信息,请参阅自定义字符编码

编写 null 值

若要使用 Utf8JsonWriter 编写 null 值,请调用:

  • WriteNull,用于将具有 null 的键值对编写为值。
  • WriteNullValue用于将 null 编写为 JSON 数组的元素。

对于字符串属性,如果字符串为 null,则 WriteStringWriteStringValue 等效于 WriteNullWriteNullValue

编写 Timespan、Uri 或 char 值

若要写入 TimespanUrichar 值,请将这些值格式化为字符串(例如,通过调用 ToString())并调用 WriteStringValue

使用 Utf8JsonReader

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

不能直接从 Visual Basic 代码使用 Utf8JsonReader。 有关详细信息,请参阅 Visual Basic 支持

下面的示例演示如何使用 Utf8JsonReader 类:

var options = new JsonReaderOptions
{
    AllowTrailingCommas = true,
    CommentHandling = JsonCommentHandling.Skip
};
var reader = new Utf8JsonReader(jsonUtf8Bytes, options);

while (reader.Read())
{
    Console.Write(reader.TokenType);

    switch (reader.TokenType)
    {
        case JsonTokenType.PropertyName:
        case JsonTokenType.String:
            {
                string? text = reader.GetString();
                Console.Write(" ");
                Console.Write(text);
                break;
            }

        case JsonTokenType.Number:
            {
                int intValue = reader.GetInt32();
                Console.Write(" ");
                Console.Write(intValue);
                break;
            }

            // Other token types elided for brevity
    }
    Console.WriteLine();
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

前面的代码假设 jsonUtf8 变量是包含有效 JSON(编码为 UTF-8)的字节数组。

使用 Utf8JsonReader 筛选数据

下面的示例演示如何同步读取文件并搜索值。

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

namespace SystemTextJsonSamples
{
    public class Utf8ReaderFromFile
    {
        private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
        private static ReadOnlySpan<byte> Utf8Bom => new byte[] { 0xEF, 0xBB, 0xBF };

        public static void Run()
        {
            // ReadAllBytes if the file encoding is UTF-8:
            string fileName = "UniversitiesUtf8.json";
            ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);

            // Read past the UTF-8 BOM bytes if a BOM exists.
            if (jsonReadOnlySpan.StartsWith(Utf8Bom))
            {
                jsonReadOnlySpan = jsonReadOnlySpan.Slice(Utf8Bom.Length);
            }

            // Or read as UTF-16 and transcode to UTF-8 to convert to a ReadOnlySpan<byte>
            //string fileName = "Universities.json";
            //string jsonString = File.ReadAllText(fileName);
            //ReadOnlySpan<byte> jsonReadOnlySpan = Encoding.UTF8.GetBytes(jsonString);

            int count = 0;
            int total = 0;

            var reader = new Utf8JsonReader(jsonReadOnlySpan);

            while (reader.Read())
            {
                JsonTokenType tokenType = reader.TokenType;

                switch (tokenType)
                {
                    case JsonTokenType.StartObject:
                        total++;
                        break;
                    case JsonTokenType.PropertyName:
                        if (reader.ValueTextEquals(s_nameUtf8))
                        {
                            // Assume valid JSON, known schema
                            reader.Read();
                            if (reader.GetString()!.EndsWith("University"))
                            {
                                count++;
                            }
                        }
                        break;
                }
            }
            Console.WriteLine($"{count} out of {total} have names that end with 'University'");
        }
    }
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

若要获取此示例的异步版本,请参阅 .NET 示例 JSON 项目

前面的代码:

  • 假设 JSON 包含一个对象数组,并且每个对象都可能包含一个字符串类型的“name”属性。

  • 对对象以及以“University”结尾的属性值进行计数。

  • 假设文件编码为 UTF-16,并将它转码为 UTF-8。 可以使用以下代码,将编码为 UTF-8 的文件直接读入 ReadOnlySpan<byte>

    ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
    

    如果文件包含 UTF-8 字节顺序标记 (BOM),请在将字节传递给 Utf8JsonReader 之前将它删除,因为读取器需要文本。 否则,BOM 被视为无效 JSON,读取器将引发异常。

下面是前面的代码可以读取的 JSON 示例。 生成的摘要消息为“2 out of 4 have names that end with 'University'”:

[
  {
    "web_pages": [ "https://contoso.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "contoso.edu" ],
    "name": "Contoso Community College"
  },
  {
    "web_pages": [ "http://fabrikam.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "fabrikam.edu" ],
    "name": "Fabrikam Community College"
  },
  {
    "web_pages": [ "http://www.contosouniversity.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "contosouniversity.edu" ],
    "name": "Contoso University"
  },
  {
    "web_pages": [ "http://www.fabrikamuniversity.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "fabrikamuniversity.edu" ],
    "name": "Fabrikam University"
  }
]

使用解码的 JSON 字符串

从 .NET 7 开始,可以使用 Utf8JsonReader.CopyString 方法而不是 Utf8JsonReader.GetString() 来使用解码的 JSON 字符串。 与始终分配新字符串的 GetString() 不同,CopyString 允许将未转义的字符串复制到你拥有的缓冲区中。 以下代码片段显示了通过 CopyString 使用 UTF-16 字符串的示例。

int valueLength = reader.HasReadOnlySequence 
    ? checked((int)ValueSequence.Length) 
    : ValueSpan.Length;

char[] buffer = ArrayPool<char>.Shared.Rent(valueLength);
int charsRead = reader.CopyString(buffer);
ReadOnlySpan<char> source = buffer.Slice(0, charsRead);

// Handle the unescaped JSON string.
ParseUnescapedString(source);
ArrayPool<char>.Shared.Return(buffer, clearArray: true);

使用 Utf8JsonReader 从流中读取内容

当读取大型文件(例如,1 GB 或更大的文件)时,你可能希望避免一次性将整个文件加载到内存中。 此时,可以使用 FileStream

使用 Utf8JsonReader 从流中读取时,适用以下规则:

  • 包含部分 JSON 有效负载的缓冲区必须至少与其中的最大 JSON 令牌一样大,以便读取器可以推进进度。
  • 缓冲区的大小必须至少与 JSON 中的最大空格序列一样大。
  • 读取器不会跟踪已读取的数据,直到它完全读取 JSON 有效负载中的下一个 TokenType。 因此,当缓冲区中有剩余字节时,必须再次将它们传递给读取器。 你可以使用 BytesConsumed 来确定剩余的字节数。

下面的代码演示了如何从流中读取。 本示例显示了 MemoryStream。 类似的代码将使用 FileStream,当 FileStream 在开头包含 UTF-8 BOM 时除外。 在这种情况下,需要先从缓冲区中去除这三个字节,然后再将剩余字节传递到 Utf8JsonReader。 否则,读取器将引发异常,因为 BOM 不被视为 JSON 的有效部分。

示例代码从 4 KB 缓冲区开始,每当发现大小不足以容纳完整的 JSON 令牌(必须容纳完整的令牌,读取器才能推动处理 JSON 有效负载)时,就会使缓冲区大小成倍增加。 仅当设置的初始缓冲区非常小(例如 10 个字节)时,代码片段中提供的 JSON 示例才会触发缓冲区大小增加。 如果将初始缓冲区大小设置为 10,则 Console.WriteLine 语句会说明缓冲区大小增加的原因和影响。 在初始缓冲区大小为 4KB 时,每个 Console.WriteLine 都会显示整个示例 JSON,并且不必增加缓冲区大小。

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

namespace SystemTextJsonSamples
{
    public class Utf8ReaderPartialRead
    {
        public static void Run()
        {
            var jsonString = @"{
                ""Date"": ""2019-08-01T00:00:00-07:00"",
                ""Temperature"": 25,
                ""TemperatureRanges"": {
                    ""Cold"": { ""High"": 20, ""Low"": -10 },
                    ""Hot"": { ""High"": 60, ""Low"": 20 }
                },
                ""Summary"": ""Hot"",
            }";

            byte[] bytes = Encoding.UTF8.GetBytes(jsonString);
            var stream = new MemoryStream(bytes);

            var buffer = new byte[4096];

            // Fill the buffer.
            // For this snippet, we're assuming the stream is open and has data.
            // If it might be closed or empty, check if the return value is 0.
            stream.Read(buffer);

            // We set isFinalBlock to false since we expect more data in a subsequent read from the stream.
            var reader = new Utf8JsonReader(buffer, isFinalBlock: false, state: default);
            Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");

            // Search for "Summary" property name
            while (reader.TokenType != JsonTokenType.PropertyName || !reader.ValueTextEquals("Summary"))
            {
                if (!reader.Read())
                {
                    // Not enough of the JSON is in the buffer to complete a read.
                    GetMoreBytesFromStream(stream, buffer, reader);
                }
            }

            // Found the "Summary" property name.
            Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
            while (!reader.Read())
            {
                // Not enough of the JSON is in the buffer to complete a read.
                GetMoreBytesFromStream(stream, buffer, reader);
            }
            // Display value of Summary property, that is, "Hot".
            Console.WriteLine($"Got property value: {reader.GetString()}");
        }

        private static void GetMoreBytesFromStream(
            MemoryStream stream, byte[] buffer, Utf8JsonReader reader)
        {
            int bytesRead;
            if (reader.BytesConsumed < buffer.Length)
            {
                ReadOnlySpan<byte> leftover = buffer.AsSpan((int)reader.BytesConsumed);

                if (leftover.Length == buffer.Length)
                {
                    Array.Resize(ref buffer, buffer.Length * 2);
                    Console.WriteLine($"Increased buffer size to {buffer.Length}");
                }

                leftover.CopyTo(buffer);
                bytesRead = stream.Read(buffer.AsSpan(leftover.Length));
            }
            else
            {
                bytesRead = stream.Read(buffer);
            }
            Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
            reader = new Utf8JsonReader(buffer, isFinalBlock: bytesRead == 0, reader.CurrentState);
        }
    }
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

前面的示例未对缓冲区大小的增长设置任何限制。 如果令牌大小太大,则代码可能会失败,并出现 OutOfMemoryException 异常。 如果 JSON 包含大小约为 1 GB 或更大的令牌,则会发生这种情况,因为将 1 GB 大小加倍会导致令牌太大,无法放入 int32 缓冲区。

Utf8JsonReader 是 ref struct

由于 Utf8JsonReader 类型是 ref struct,因此它具有某些限制。 例如,它无法作为字段存储在 ref struct 之外的类或结构中。 若要实现高性能,此类型必须为 ref struct,因为它需要缓存输入 ReadOnlySpan<byte>(这本身便是 ref struct)。 此外,此类型是可变的,因为它包含状态。 因此,它按引用传递而不是按值传递。 按值传递会产生结构副本,状态更改会对调用方不可见。 有关如何使用 ref struct 的详细信息,请参阅编写安全有效的 C# 代码

读取 UTF-8 文本

若要在使用 Utf8JsonReader 时实现可能的最佳性能,请读取已编码为 UTF-8 文本(而不是 UTF-16 字符串)的 JSON 有效负载。 有关代码示例,请参阅使用 Utf8JsonReader 筛选数据

使用多段 ReadOnlySequence 进行读取

如果 JSON 输入是 <>,则在运行读取循环时,可以从读取器上的 ValueSpan 属性访问每个 JSON 元素。 但是,如果输入是 ReadOnlySequence<byte>(这是从 PipeReader 读取的结果),则某些 JSON 元素可能会跨 ReadOnlySequence<byte> 对象的多个段。 无法在连续内存块中从 ValueSpan 访问这些元素。 而是在每次将多段 ReadOnlySequence<byte> 作为输入时,轮询读取器上的 HasValueSequence 属性,以确定如何访问当前 JSON 元素。 下面是推荐模式:

while (reader.Read())
{
    switch (reader.TokenType)
    {
        // ...
        ReadOnlySpan<byte> jsonElement = reader.HasValueSequence ?
            reader.ValueSequence.ToArray() :
            reader.ValueSpan;
        // ...
    }
}

使用 ValueTextEquals 进行属性名称查找

不要使用 ValueSpan 通过对属性名称查找调用 SequenceEqual 来执行逐字节比较。 改为调用 ValueTextEquals,因为该方法会对在 JSON 中转义的任何字符取消转义。 下面的示例演示如何搜索名为“name”的属性:

private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
while (reader.Read())
{
    switch (reader.TokenType)
    {
        case JsonTokenType.StartObject:
            total++;
            break;
        case JsonTokenType.PropertyName:
            if (reader.ValueTextEquals(s_nameUtf8))
            {
                count++;
            }
            break;
    }
}

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

内置 System.Text.Json API 仅返回不可为 null 的值类型。 例如,Utf8JsonReader.GetBoolean 返回 bool。 如果它在 JSON 中发现 Null,则会引发异常。 下面的示例演示两种用于处理 null 的方法,一种方法是返回可为 null 的值类型,另一种方法是返回默认值:

public bool? ReadAsNullableBoolean()
{
    _reader.Read();
    if (_reader.TokenType == JsonTokenType.Null)
    {
        return null;
    }
    if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
    {
        throw new JsonException();
    }
    return _reader.GetBoolean();
}
public bool ReadAsBoolean(bool defaultValue)
{
    _reader.Read();
    if (_reader.TokenType == JsonTokenType.Null)
    {
        return defaultValue;
    }
    if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
    {
        throw new JsonException();
    }
    return _reader.GetBoolean();
}

请参阅