• System.Text.Json的JsonDocument类讲解


    System.Text.Json的JsonDocument类讲解

    本文内容来自我写的开源电子书《WoW C#》,现在正在编写中,可以去WOW-Csharp/学习路径总结.md at master · sogeisetsu/WOW-Csharp (github.com)来查看编写进度。预计2021年年底会完成编写,2022年2月之前会完成所有的校对和转制电子书工作,争取能够在2022年将此书上架亚马逊。编写此书的目的是因为目前.NET市场相对低迷,很多优秀的书都是基于.NET framework框架编写的,与现在的.NET 6相差太大,正规的.NET 5学习教程现在几乎只有MSDN,可是MSDN虽然准确优美但是太过琐碎,没有过阅读开发文档的同学容易一头雾水,于是,我就编写了基于.NET 5的《WoW C#》。本人水平有限,欢迎大家去本书的开源仓库sogeisetsu/WOW-Csharp关注、批评、建议和指导。

    解析JSON字符串

    注意本文重点讲解的是解析,而非序列化,关于序列化请查看WOW-Csharp/数组和集合.md at master · sogeisetsu/WOW-Csharp (github.com)

    c#解析json字符串在.NET 5的首选是使用System.Text.JsonJsonDocument类。

    格式化输出

    想要格式化输出,需要先把字符串转变成一个JsonDocument实例化对象,然后在序列化这个对象的时候指定JsonSerializerOptions为整齐打印。

    // 先定义一个json字符串
    string jsonText = "{\"ClassName\":\"Science\",\"Final\":true,\"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\"}],\"Teacher'sName\":\"Jane\"}";
    Console.WriteLine(jsonText);
    // 将表示单个 JSON 字符串值的文本分析为 JsonDocument
    JsonDocument jsonDocument = JsonDocument.Parse(jsonText);
    // 序列化
    string formatJson = JsonSerializer.Serialize(jsonDocument, new JsonSerializerOptions()
                                                 {
                                                     // 整齐打印
                                                     WriteIndented = true,
                                                     Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
                                                 });
    // 格式化输出
    Console.WriteLine(formatJson);
    

    这个比较麻烦,我们可以将其制作成拓展方法。

    internal static class JsonDocumentExtensions
    {
        internal static string JDFormatToString(this JsonDocument jsonDocument)
        {
            return JsonSerializer.Serialize(jsonDocument, new JsonSerializerOptions()
                                            {
                                                WriteIndented = true,
                                                Encoder=JavaScriptEncoder.Create(UnicodeRanges.All)
                                            });
        }
    
        internal static string TOJsonString(this string str)
        {
            JsonDocument jsonDocument = JsonDocument.Parse(str);
            return JsonSerializer.Serialize(jsonDocument, new JsonSerializerOptions()
                                            {
                                                WriteIndented = true,
                                                Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
                                            });
        }
    }
    
    

    这样就可以用类似于下面的方法直接调用了:

    // jsondocument 格式化输出为json字符串
    string a = jsonDocument.JDFormatToString();
    // 格式化字符串
    string b = jsonText.TOJsonString();
    

    JSON DOM

    对JSON进行DOM操作.NET提供了两种官方方法,分别是JsonDocumenth和JSonNode,其中JsonNode提供了创建可变 DOM 的能力,它更加强大和简单,但是JsonNode是.NET 6的内容,鉴于.NET 6的稳定版刚刚发布,所以本文还是讲解JsonDocumenth。.NET 6是一个LTS版本,它于2021年11月8日正式发布,会支持到2024年11月8日,详情可以查看.NET and .NET Core official support policy (microsoft.com)笔者会在.NET 5结束支持之前(2022 年 5 月 8 日)写一篇关于JSonNode的文章作为本文的Patch。

    JsonDocument 提供了使用 Utf8JsonReader 构建只读 DOM 的能力。可以通过 JsonElement 类型访问组成有效负载的 JSON 元素。 JsonElement 类型提供数组和对象枚举器以及用于将 JSON 文本转换为常见 .NET 类型的 API。 JsonDocument 公开一个 RootElement 属性。

    关于JsonDocument,MSDN上有一篇非常好的讲解文章How to use a JSON document, Utf8JsonReader, and Utf8JsonWriter in System.Text.Json | Microsoft Docs,这一部分笔者更多的是采用MSDN上面的例子,此部分在某种意义上可以看作对How to use a JSON document, Utf8JsonReader, and Utf8JsonWriter in System.Text.Json的翻译。

    用作例子的Json数据如下:

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

    方法概述

    先将其反序列化成JsonDocument对象:

    Console.WriteLine("对json字符串进行dom操作");
    string jsonText = "{\"ClassName\":\"Science\",\"Final\":true,\"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\"}],\"Teacher'sName\":\"Jane\"}";
    JsonDocument jsonDocument = JsonDocument.Parse(jsonText);
    

    获取当前JsonDocument的根元素(JsonElement类型):

    JsonElement root = jsonDocument.RootElement;
    

    RootElement是json数据的根,后续所有的操作都与其息息相关。

    GetProperty根据键名,获取根元素下的元素(JsonElement类型):

    JsonElement students = root.GetProperty("Students");
    

    GetArrayLength获取数组属性的长度(如果将此方法用于非数组类型的json值会报错):

    // 获取数组长度
    Console.WriteLine(students.GetArrayLength());
    

    可以对值类型为数组的JsonElement使用EnumerateArray ()方法来获取枚举器(IEnumerator),从而进行循环操作:

    // EnumerateArray 一个枚举器,它用于枚举由该 JsonElement 表示的 JSON 数组中的值。
    foreach (JsonElement student in students.EnumerateArray())
    {
        Console.WriteLine(student);
        Console.WriteLine(student.ValueKind);// object
        // 获取属性Name的string值
        Console.WriteLine(student.GetProperty("Name").GetString());
    }
    

    获取值

    对于JsonElement获取元素值的方式比较复杂,首先需要知道值的类型,然后根据值的类型来选择方法,方法列表可以从JsonElement 结构 (System.Text.Json) | Microsoft Docs查看,比如值的类型是double,就使用GetDouble()来获取json数字(double类型):

    Console.WriteLine(student.GetProperty("Grade").GetDouble());
    

    如果当前的值是string类型,就使用GetString()来获取json字符串:

    Console.WriteLine(semester.GetString());
    

    总之,为了获取准确的json值,必须提前知道json值的类型。这样才会最大限度保证不会出错。

    获取和判读Json值的类型

    可以使用JsonElementValueKind属性来获取值类型:

    Console.WriteLine(students.ValueKind);
    

    ValueKind属性的类型是名为JsonValueKind的枚举类型。JsonValueKind的字段如下:

    Array2JSON 数组。
    False 6 JSON 值 false
    Null 7 JSON 值 null
    Number 4 JSON 数字。
    Object 1 JSON 对象。
    String 3 JSON 字符串。
    True 5 JSON 值 true
    Undefined 0 没有值(不同于 Null)。

    故可以使用像下面这种判断相等的方式来检测数据类型:

    Console.WriteLine(students.ValueKind == JsonValueKind.Array); // true
    

    检查属性是否存在

    可以使用TryGetProperty方法来根据键来判断元素是否存在,demo如下:

    root.TryGetProperty("Name", out JsonElement value)
    

    存在就返回true,不存在就返回false,TryGetProperty的第一个参数是键名,第二个参数是用out关键字修饰的JsonElement类型,如果此属性存在,会将其值分配给 value 参数。

    借助TryGetProperty既可以判断属性是否存在,也能在属性存在的情况下获取该属性对应的JsonElement,demo:

    // 检查存在
    Console.WriteLine(root.TryGetProperty("Semester", out JsonElement value));
    // 使用被分配的JsonElement
    Console.WriteLine(value.GetString());
    

    MSDN的demo更具备实用性:

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

    如何在 JsonDocument 和 JsonElement 中搜索子元素

    对 JsonElement 的搜索需要对属性进行顺序搜索,因此速度相对较慢(例如在使用 TryGetProperty 时)。 System.Text.Json 旨在最小化初始解析时间而不是查找时间。因此,在搜索 JsonDocument 对象时使用以下方法来优化性能:

    • 使用内置的枚举器(EnumerateArray 和 EnumerateObject)而不是自己做索引或循环。不要对数组形式的JsonElement进行诸如students[1]的操作。
    • 不要使用 RootElement 通过每个属性对整个 JsonDocument 进行顺序搜索。相反,根据 JSON 数据的已知结构搜索嵌套的 JSON 对象。也就是说不要进行不必要的搜索,要根据自己对所操作的JSON的最大了解程度来进行搜索,比如明知道某个json数组里面没有自己想要的数据,就别去对它进行一遍又一遍的搜索

    JsonDocument 是非托管资源

    因为是非托管资源,其不会在CLR中被托管。JsonDocument 类型实现了 IDisposable ,为了内存的整洁应该在using块中使用,使其在使用完之后立即被释放资源,就像下面这样:

    using (JsonDocument jsonDocument = JsonDocument.Parse(jsonText))
    {
        // 对jsonDocument的各种操作
    }
    

    前面笔者没有将其放在using块里面纯粹是为了演示方便,请大家注意在using块中使用JsonDocument 才是推荐的用法。

    函数调用JsonDocument

    如果因为某种原因需要将JsonDocument转给方法调用者,则仅从您的 API 返回 JsonDocument,在大多数情况下,这不是必需的。返回返回 RootElement 的 Clone就好,它是一个 JsonElement。demo如下:

    public JsonElement LookAndLoad(JsonElement source)
    {
        string json = File.ReadAllText(source.GetProperty("fileName").GetString());
    
        using (JsonDocument doc = JsonDocument.Parse(json))
        {
            return doc.RootElement.Clone();
        }
    }
    
    

    如果你所编写的方法收到一个 JsonElement 并返回一个子元素,则没有必要返回子元素的克隆。调用者负责使传入的 JsonElement 所属的 JsonDocument 保持活动状态即可。demo如下:

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

    LICENSE

    已将所有引用其他文章之内容清楚明白地标注,其他部分皆为作者劳动成果。对作者劳动成果做以下声明:

    copyright © 2021 苏月晟,版权所有。

    知识共享许可协议
    作品由苏月晟采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

    本文作者:苏月晟,转载请注明原文链接:https://www.cnblogs.com/sogeisetsu/p/15631408.html

    我的开源电子书《WoW C#》正在编写中,欢迎去https://git.io/JMlrA 关注、批评和指导。
  • 相关阅读:
    无重复字符的最长子串
    最长公共前缀
    项目开发的 工程化
    包管理 import debug 模块管理 module
    Third Party Browser Drivers NOT DEVELOPED by seleniumhq
    任何不看源码的代码引入都是存在定时爆炸的可能
    博客数计数
    lineage 世系 血缘 容错机制 DAG
    查源码分析 游标 写 需要 cursors 一切不看源码的代码引入都是定时炸弹的启动
    8核 16g 及时释放内存空间
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/15890707.html
Copyright © 2020-2023  润新知