65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 中使用 System.Text.Json

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2022 年 8 月 14 日

CPOL

13分钟阅读

viewsIcon

47377

downloadIcon

640

使用简单的 JSON 对象和集合到自定义转换器以及转换为 .NET 类 - System.Text.Json

使用 JSON 系列

下载

引言

虽然 JSON 是一种紧凑且易于阅读的跨语言存储和数据交换格式,但它提供的灵活性有时需要一些自定义处理来解析数据。

如果您不熟悉 JSON,那么这里是来自官方 http://www.json.org 的定义:

引用

JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。它易于人类阅读和书写。易于机器解析和生成。它基于 JavaScript 编程语言的一个子集,标准 ECMA-262 第 3 版 - 1999 年 12 月。

Microsoft 对新的 System.Text.Json API 的解释

引用

System.Text.Json 主要侧重于性能、安全性和标准合规性。它的默认行为有一些关键差异,并且不旨在与 NewtonSoft.Json 具备相同的功能。在某些场景下,System.Text.Json 目前没有内置功能,但有推荐的解决方法。在其他场景下,解决方法不切实际。

在下一篇文章中,我将介绍这些“目前没有内置功能”的功能之一,因为它超出了本文的范围。

目录

背景

上一篇文章中,我们讨论了“在 C# 和 VB 中使用 Newtonsoft.Json”。该文章涵盖了从简单的 JSON 对象和集合到自定义转换器、无效集合属性名称以及在反序列化为类时进行转换。

本文将通过移植旧的 NewtonSoft.Json 示例项目来探讨新的 System.Text.Json API。

本文的结构以及示例项目的结构将遵循上一篇文章的结构。这样做的目的是,如果您只对新 API 感兴趣,同时也方便那些想要比较 NewtonSoft.JsonSystem.Text.Json 并了解迁移过程所需内容的人。

VB 限制

我想在这篇文章中包含 VB 示例代码,就像我在上一篇文章中为 NewtonSoft.Json 所做的那样。然而,在撰写本文时,6.0 版的 .NET (Core) Ref Struct 类型,用于 Utf8JsonReaderSpan<T>,在 VB 中不受支持。

因此,对于本文,我们将只使用 C#。您可以通过使用单独的 C# 库来实现自定义转换器的解决方法。本文不会涵盖这一点。但是,如果您想这样做,则需要将转换器和模型/POCO 类放在同一个类库项目中,以避免循环引用。

目前,如果您使用 VB,我建议在您的项目中继续使用 NewtonSoft.Json

NewtonSoft.Json 迁移到 System.Text.Json

NewtonSoft.Json 迁移到 System.Text.Json 时,存在许多差异。Microsoft 有迁移文档,其中涵盖了这些差异。

主要区别是:

引用

System.Text.Json 默认严格,避免调用者的任何猜测或解释,强调确定性行为。该库的设计故意如此,以提高性能和安全性。Newtonsoft.Json 默认灵活。这种基本的设计差异是以下许多特定默认行为差异的根源。

工具和库

像任何事物一样,您需要合适的工具来完成工作。这里有一些可用的工具,包括本文中使用的工具。

查看器和验证器

有时,JSON 数据是压缩的,不易阅读,或者我们需要验证原始数据。

代码生成器

我们需要创建一个类结构来转换原始 JSON 数据。您可以手动从 JSON 文件创建类,这是一个非常缓慢且耗时的任务。有更快捷的方法可以完成此操作。这里有几个:

  • quicktype.io - 支持 C#、TypeScript、Go、Java、Elm、Swift、简单类型和模式
  • JSON Utils - 支持 VB 和 C#,提供许多选项

注意:在撰写本文时,我找不到任何专门支持 System.Text.Json 类/属性特性的生成器。

数据转换

一旦您拥有了原始 JSON 数据并创建了映射数据的类,下一步将是反序列化到类和从类序列化。本文将重点介绍反序列化。

以下辅助类是处理空结果的干净实现。

public static class JsonHelper
{
    /// <summary>
    /// Convert Class to Json object (string)
    /// </summary>
    /// <typeparam name="TClass">Class type to be serialized</typeparam>
    /// <param name="data">Class to serialize</param>
    /// <param name="isEmptyToNull">true = return null if empty; 
    /// false empty Json object</param>
    /// <param name="options">JsonSerializer options</param>
    /// <returns>Json encoded string</returns>
    public static string FromClass<TClass>
    (TClass data, bool isEmptyToNull = false, JsonSerializerOptions? options = null)
        where TClass : class
    {
        string response = string.Empty;

        if (!EqualityComparer<TClass>.Default.Equals(data, default))
            response = JsonSerializer.Serialize(data, options: options);

        return isEmptyToNull ? response == "{}" ? "null" : response : response;
    }

    /// <summary>
    /// Convert a Json object (string) to a class
    /// </summary>
    /// <typeparam name="TClass">Class type to be deserialized into</typeparam>
    /// <param name="data">Json string to be deserialized</param>
    /// <param name="options">JsonSerializer options</param>
    /// <returns>Deserialized class of TClass</returns>
    public static TClass? ToClass<TClass>(string data, 
                          JsonSerializerOptions? options = null)
        where TClass : class
    {
        TClass? response = default(TClass);

        return string.IsNullOrEmpty(data)
            ? response
            : JsonSerializer.Deserialize<TClass>(data, options ?? null);
    }
}

标准数据类型

让我们从一些简单的开始。以下两个示例使用 .NET 的原始数据和集合类型。

简单对象类型

这是来自 Etsy API 的 v2 Category JSON 对象。所有 JSON 字段都映射到 .NET 的原始数据类型。

{
        "category_id": 68890752,
        "name": "gloves",
        "meta_title": "Handmade Gloves on Etsy - Gloves, mittens, arm warmers",
        "meta_keywords": "handmade gloves, gloves, handmade arm warmers, 
         handmade fingerless gloves, handmade mittens, hand knit mittens, 
         hand knit gloves, handmade accessories",
        "meta_description": "Shop for unique, handmade gloves on Etsy, 
         a global handmade marketplace. Browse gloves, arm warmers, 
         fingerless gloves & more from independent artisans.",
        "page_description": "Shop for unique, 
         handmade gloves from our artisan community",
        "page_title": "Handmade gloves",
        "category_name": "accessories\/gloves",
        "short_name": "Gloves",
        "long_name": "Accessories > Gloves",
        "num_children": 3
}

System.Text.Json 支持通过 JsonSerializerOptions 启用不区分大小写的属性名匹配。

var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true
};

反序列化时,您将选项传递给序列化器。

public Category Result { get; set; }

private const string fileName = "Etsy_Category.Json";
private readonly string filePath = Environment.CurrentDirectory;

private void GetData()
{
    // Retrieve JSON data from file
    var rawJson = File.ReadAllText(Path.Combine(filePath, fileName));

    // Convert to C# Class typed object
    Result = JsonHelper.ToClass<Category>(rawJson, options);
}

现在您可以使用 POCO(Plain Old Class Objects)了。

public class Category
{
    public int? CategoryId { get; set; }
    public string? Name { get; set; }
    public string? MetaTitle { get; set; }
    public string? MetaKeywords { get; set; }
    public string? MetaDescription { get; set; }
    public string? PageDescription { get; set; }
    public string? PageTitle { get; set; }
    public string? CategoryName { get; set; }
    public string? ShortName { get; set; }
    public string? LongName { get; set; }
    public int? NumChildren { get; set; }
}

如果您想使用属性进行手动映射,请使用 JsonPropertyName 属性,以前是 Newtonsoft 的 JsonProperty

public class Category
{
    [JsonPropertyName("category_id")]
    public int? CategoryId { get; set; }

    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("meta_title")]
    public string? MetaTitle { get; set; }

    [JsonPropertyName("meta_keywords")]
    public string? MetaKeywords { get; set; }

    [JsonPropertyName("meta_description")]
    public string? MetaDescription { get; set; }

    [JsonPropertyName("page_description")]
    public string? PageDescription { get; set; }

    [JsonPropertyName("page_title")]
    public string? PageTitle { get; set; }

    [JsonPropertyName("category_name")]
    public string? CategoryName { get; set; }

    [JsonPropertyName("short_name")]
    public string? ShortName { get; set; }

    [JsonPropertyName("long_name")]
    public string? LongName { get; set; }

    [JsonPropertyName("num_children")]
    public int? NumChildren { get; set; }
}

现在我们可以将 JSON 数据反序列化为 .NET 类。

public Category Result { get; set; }

private const string fileName = "Etsy_Category.Json";
private readonly string filePath = Environment.CurrentDirectory;

private void GetData()
{
    // Retrieve JSON data from file
    var rawJson = File.ReadAllText(Path.Combine(filePath, fileName));

    // Convert to C# Class typed object
    Result = JsonHelper.ToClass<Category>(rawJson);
}

然后就可以处理数据了。这是附带的示例应用程序(WinFormSimpleObject)的屏幕截图。

简单集合类型

Etsy API,与其他许多 API 一样,不仅处理单个对象,还处理 JSON 响应中包装的对象集合。

{
    "count": 27,
    "results": [{
        "category_id": 68890752,
        "name": "gloves",
        "meta_title": "Handmade Gloves on Etsy - Gloves, mittens, arm warmers",
        "meta_keywords": "handmade gloves, gloves, handmade arm warmers, 
         handmade fingerless gloves, handmade mittens, hand knit mittens, 
         hand knit gloves, handmade accessories",
        "meta_description": "Shop for unique, handmade gloves on Etsy, 
         a global handmade marketplace. Browse gloves, arm warmers, 
         fingerless gloves & more from independent artisans.",
        "page_description": "Shop for unique, 
         handmade gloves from our artisan community",
        "page_title": "Handmade gloves",
        "category_name": "accessories\/gloves",
        "short_name": "Gloves",
        "long_name": "Accessories > Gloves",
        "num_children": 3
    },
    {
        "category_id": 68890784,
        "name": "mittens",
        "meta_title": "Handmade Mittens on Etsy - Mittens, gloves, arm warmers",
        "meta_keywords": "handmade mittens, handcrafted mittens, mittens, 
         accessories, gloves, arm warmers, fingerless gloves, mittens, 
         etsy, buy handmade, shopping",
        "meta_description": "Shop for unique, handmade mittens on Etsy, 
         a global handmade marketplace. Browse mittens, arm warmers, 
         fingerless gloves & more from independent artisans.",
        "page_description": "Shop for unique, 
         handmade mittens from our artisan community",
        "page_title": "Handmade mittens",
        "category_name": "accessories\/mittens",
        "short_name": "Mittens",
        "long_name": "Accessories > Mittens",
        "num_children": 4
    }],
    "params": {
        "tag": "accessories"
    },
    "type": "Category",
    "pagination": {
        
    }
}

这是我们的响应包装器。

public class Pagination
{
    [JsonPropertyName("effective_limit")]
    public int? EffectiveLimit { get; set; }

    [JsonPropertyName("effective_offset")]
    public int? EffectiveOffset { get; set; }

    [JsonPropertyName("effective_page")]
    public int? EffectivePage { get; set; }

    [JsonPropertyName("next_offset")]
    public int? NextOffset { get; set; }

    [JsonPropertyName("next_page")]
    public int? NextPage { get; set; }
}

public class Params
{
    [JsonPropertyName("tag")]
    public string? Tag { get; set; }
}

public class Response<TModel> where TModel : class
{
    [JsonPropertyName("count")]
    public int? Count { get; set; }

    [JsonPropertyName("results")]
    public IList<TModel>? Results { get; set; }

    [JsonPropertyName("params")]
    public Params? Params { get; set; }

    [JsonPropertyName("type")]
    public string? Type { get; set; }

    [JsonPropertyName("pagination")]
    public Pagination? Pagination { get; set; }
}

现在我们可以将 JSON 数据反序列化为 .NET 类。

public BindingList<Category> Categories { get; } = new();

public void HandleClicked() => GetData();

private void GetData()
{
    // Retrieve JSON data from file
    string rawJson = File.ReadAllText(Path.Combine(filePath, fileName));

    // Convert to C# Class typed object
    Response<Category>? response = JsonHelper.ToClass<Response<Category>>(rawJson);

    // Get collection of objects
    if (response is { Results.Count: > 0 })
    {
        IList<Category>? data = response.Results;

        Categories.Clear();

        for (int i = 0; i < data.Count; i++)
            Categories.Add(data[i]);
    }
}

现在我们可以处理数据了。这是附带的示例应用程序(WinFormSimpleCollection)的屏幕截图。

非标准类型和数据结构类型

并非所有平台上的所有语言都具有兼容的数据类型。此外,支持多种数据格式的提供商并不总是能在数据格式之间进行清晰的转换。下一节将涵盖这些问题并用简单的解决方案来解决它们。

NewtonSoft.Json 不同,System.Text.Json 是从头重写的,并使用强类型的基础 JsonConverter。这打破了与 NewtonSoft.Json 转换器的兼容性。我已移植上一篇文章中的转换器,以便进行比较。我将根据经验添加注释。

UNIX 纪元时间戳

什么是 UNIX 纪元时间戳?根据 Wikipedia.org

引用

一种描述时间瞬间的系统,定义为自 1970 年 1 月 1 日星期四 00:00:00 协调世界时 (UTC) 起经过的秒数 [1][注 1],减去自那时以来发生的闰秒数。这是来自 Twitter 的一个示例:

"reset": 1502612374

这是来自 Flickr 的一个示例:

"lastupdate": "1502528455"

我们可以有一个整数属性字段,并在反序列化后将整数纪元时间戳转换为 DateTime 类型。替代方案,也是更好的解决方案,是使用自定义 JsonConverter 属性。

internal static class Unix
{
    internal static readonly DateTime Epoch = new DateTime
    (year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0, 
     kind: DateTimeKind.Utc);
}

public static class DoubleExtensions
{
    public static DateTime FromUnixDate(this double? unixDate)
    {
        return Unix.Epoch.AddSeconds(unixDate ?? 0.0);
    }

    public static DateTime FromUnixDate(this double unixDate)
    {
        return Unix.Epoch.AddSeconds(unixDate);
    }
}

public sealed class JsonUnixDateConverter : JsonConverter<DateTime?>
{
    public override DateTime? Read(ref Utf8JsonReader reader, 
                    Type typeToConvert, JsonSerializerOptions options)
        => reader.TokenType switch
        {
            JsonTokenType.Number => reader.GetDouble().FromUnixDate(),
            JsonTokenType.String => double.TryParse(reader.GetString(), out var value)
                ? value.FromUnixDate()
                : typeToConvert == typeof(DateTime)
                    ? default
                    : null,
            _ => throw new JsonException()
        };

    public override void Write(Utf8JsonWriter writer, 
           DateTime? value, JsonSerializerOptions options)
        => throw new NotImplementedException();
}

工作原理

由于转换器是强类型的,如果您尝试将转换器用于错误的数据类型,则会在运行时抛出 System.InvalidOperationExceptionJsonUnixDateConverter.Read 方法执行,解析值,从 UNIX 纪元转换为 .NET DateTime 类型,并将值返回以分配给类属性。

如何使用

只需将 JsonUnixDateConverter 应用到属性上即可。

[JsonPropertyName("dateuploaded"), JsonConverter(typeof(JsonUnixDateConverter))]
public DateTime? DateUploaded { get; set; }

数据结构类型

一些数据提供者支持多种数据格式:XML、JSON 等,并且格式上的差异可能会产生一些有趣的数据结构类型。数据结构类型是指单个变量类型被描述为一个对象而不是一个简单值。

Flickr 有很多这方面的例子,其中 XML 不直接翻译 - XML 同时具有属性和元素来描述数据,而 JSON 只有字段。一个例子是 Photo 对象和评论计数字段。

{
    "photo": {
        "comments": {
            "_content": "483"
        }
    }
}

如果我们进行一对一的翻译,所需的类将是:

public class Comments
{
    [JsonPropertyName("_content")]
    public int Count { get; set; }
}

public class Photo
{
    [JsonPropertyName("comments")]
    public Comments Comments { get; set; }
}

然后使用上述类结构:

int GetCommentCount(Photo photo)
{
    return photo.Comments.Count;
}

如果能将评论计数简化为单个整数而不是类对象,那就更好了。

int GetCommentCount(Photo photo)
{
    return photo.CommentCount;
}

Flickr 有许多其他值数据类型的工作方式相同。例如,照片的 title 字段。

"title": {
    "_content": "North korean army Pyongyang North Korea \ubd81\ud55c"
}

解决方案是通用的 JsonConverter

public class JsonFlickrContentConverter<TObject> : JsonConverter<TObject>
{
    public override TObject? Read(ref Utf8JsonReader reader, 
           Type typeToConvert, JsonSerializerOptions options)
    {
        // position reader

        reader.Read(); // read Property TObject_property
        reader.Read(); // read Property "_content"

        // Get the value of "_content" and convert to the correct type
        TObject? result = (TObject?)Convert.ChangeType(reader.GetString(), 
                           GetUnderlyingType());
        
        reader.Read(); // read EndObject for TObject

        return result;
    }

    // converts Generic nullable type to underlying type
    // eg: int? to int
    private Type GetUnderlyingType()
    {
        Type type = typeof(TObject);
        return Nullable.GetUnderlyingType(type) ?? type;
    }

    public override void Write(Utf8JsonWriter writer, 
           TObject? value, JsonSerializerOptions options)
        => throw new NotImplementedException();
}

然后使用:

public class Photo
{
    [JsonPropertyName("comments"), 
     JsonConverter(typeof(JsonFlickrContentConverter<int?>))]
    public int? Comments { get; set; }
    
    [JsonPropertyName("title"), 
     JsonConverter(typeof(JsonFlickrContentConverter<string?>))]
    public string? Title { get; set; }
}

注意:这里,我们定义了 JsonFlickrContentConverter 的返回类型。对于注释,我们使用 int?,对于 Title,我们使用 string?。我在这里使用了可空类型,但是,您可以使用任何类型,只要转换器类型与属性类型匹配即可。

展平集合类型

与数据结构类型类似,对象集合有时也无法很好地从 XML 转换为 JSON。Flickr 有许多这方面的例子。Photo.Notes 集合就是一个典型的例子。

"notes": {
    "note": [{
        "id": "72157613689748940",
        "author": "22994517@N02",
        "authorname": "morningbroken",
        "authorrealname": "",
        "authorispro": 0,
        "x": "227",
        "y": "172",
        "w": "66",
        "h": "31",
        "_content": "Maybe ~ I think  ...She is very happy ."
    },
    {
        "id": "72157622673125344",
        "author": "40684115@N06",
        "authorname": "Suvcon",
        "authorrealname": "",
        "authorispro": 0,
        "x": "303",
        "y": "114",
        "w": "75",
        "h": "60",
        "_content": "this guy is different."
    }]
},

类结构将是:

public class Photo
{
    [JsonPropertyName("notes")]
    public Notes Notes { get; set; }
}

public class Notes
{
    [JsonPropertyName("note")]
    public List<Note> Note { get; set; }
}

正如您所见,我们最终得到了一个额外的、不想要的 Notes 类来保存 Note 列表。

我们可以使用自定义 JsonConverter 来折叠数据结构,从而简化所需的类。

public class JsonFlickrCollectionConverter<TModel> : 
             JsonConverter<List<TModel>?> where TModel : class
{
    public override List<TModel> Read(ref Utf8JsonReader reader, 
           Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();

        // position reader

        reader.Read(); // move to property name
        reader.Read(); // move to start of the array

        if (reader.TokenType != JsonTokenType.StartArray)
            throw new JsonException();

        List<TModel> items = new();

        // Walk through all of the items in the array
        while (reader.TokenType != JsonTokenType.EndArray)
            items.AddRange(JsonSerializer.Deserialize<List<TModel>>
                                          (ref reader, options)!);

        reader.Read(); // move to EndObject

        return items;
    }

    public override void Write(Utf8JsonWriter writer, 
           List<TModel>? value, JsonSerializerOptions options)
        => throw new NotImplementedException();
}

然后使用:

[JsonPropertyName("notes"), JsonConverter(typeof(JsonFlickrCollectionConverter<Note>))]
public List<Note>? Notes { get; set; }

多值类型集合

也称为多态反序列化,是指返回的对象集合是相似或不同复杂对象类型的。themoviedb.org Multi-search 在同一集合中返回 TVMoviePerson 类型。Google Drive 是另一个返回包含 文件和文件夹 复杂对象类型的集合的例子。

MovieDB.Org

这是包含的 MovieDB 演示应用程序的屏幕截图,它显示了这种情况的一个示例(WpfMultiSearch)。

上面屏幕截图的 JSON 数据如下所示:

{
    "page": 1,
    "total_results": 3433,
    "total_pages": 172,
  "results": [
    {
      "original_name": "Captain N and the New Super Mario World",
      "id": 26732,
      "media_type": "tv",
      "name": "Captain N and the New Super Mario World",
      "vote_count": 2,
      "vote_average": 3.5,
      "poster_path": "/i4Q8a0Ax5I0h6b1rHOcQEZNvJzG.jpg",
      "first_air_date": "1991-09-14",
      "popularity": 1.479857,
      "genre_ids": [
        16,
        35
      ],
      "original_language": "en",
      "backdrop_path": "/iYT5w3Osv3Bg1NUZdN9UYmVatPs.jpg",
      "overview": "Super Mario World is an American animated television series 
       loosely based on the Super NES video game of the same name. 
       It is the third and last Saturday morning cartoon based on the Super Mario Bros. 
       NES and Super NES series of video games. The show only aired 13 episodes 
       due to Captain N: The Game Master's cancellation on NBC. 
       Just like The Adventures of Super Mario Bros. 3, the series is produced by 
       DIC Entertainment and Reteitalia S.P.A in association with Nintendo, 
       who provided the characters and power-ups from the game.",
      "origin_country": [
        "US"
      ]
    },
    {
      "popularity": 1.52,
      "media_type": "person",
      "id": 1435599,
      "profile_path": null,
      "name": "Small World",
      "known_for": [
        {
          "vote_average": 8,
          "vote_count": 1,
          "id": 329083,
          "video": false,
          "media_type": "movie",
          "title": "One For The Road: Ronnie Lane Memorial Concert",
          "popularity": 1.062345,
          "poster_path": "/i8Ystwg81C3g9a5z3ppt3yO1vkS.jpg",
          "original_language": "en",
          "original_title": "One For The Road: Ronnie Lane Memorial Concert",
          "genre_ids": [
            10402
          ],
          "backdrop_path": "/oG9uoxtSuokJBgGO4XdC5m4uRGU.jpg",
          "adult": false,
          "overview": "At The Royal Albert Hall, London on 8th April 2004 
           after some 15 months of planning with Paul Weller, Ronnie Wood, 
           Pete Townshend, Steve Ellis, Midge Ure, Ocean Colour Scene amongst them 
           artists assembled to perform to a sell-out venue and to pay tribute to a man 
           who co-wrote many Mod anthems such as \"\"Itchycoo Park, All Or Nothing, 
           Here Comes The Nice, My Mind's Eye\"\" to name just a few. 
           Ronnie Lane was the creative heart of two of Rock n Rolls quintessentially 
           English groups, firstly during the 60's with The Small Faces then 
           during the 70;s with The Faces. After the split of the Faces he then 
           formed Slim Chance and toured the UK in a giant circus tent as well as 
           working in the studio with Eric Clapton, Pete Townshend and Ronnie Wood. 
           5,500 fans looked on in awe at The R.A.H 
           as the superb evening's entertainment 
           ended with \"\"All Or Nothing\"\" featuring a surprise appearance by 
           Chris Farlowe on lead vocals.",
          "release_date": "2004-09-24"
        }
      ],
      "adult": false
    },
    {
      "vote_average": 6.8,
      "vote_count": 4429,
      "id": 76338,
      "video": false,
      "media_type": "movie",
      "title": "Thor: The Dark World",
      "popularity": 10.10431,
      "poster_path": "/bnX5PqAdQZRXSw3aX3DutDcdso5.jpg",
      "original_language": "en",
      "original_title": "Thor: The Dark World",
      "genre_ids": [
        28,
        12,
        14
      ],
      "backdrop_path": "/3FweBee0xZoY77uO1bhUOlQorNH.jpg",
      "adult": false,
      "overview": "Thor fights to restore order across the cosmos… 
       but an ancient race led by the vengeful Malekith returns to plunge 
       the universe back into darkness. Faced with an enemy that even Odin and Asgard 
       cannot withstand, Thor must embark on his most perilous and personal journey yet, 
       one that will reunite him with Jane Foster and force him to sacrifice everything 
       to save us all.",
      "release_date": "2013-10-29"
    }
  ]
}

识别不同复杂数据类型的关键是一个键字段。在上面的 MovieDB JSON 数据示例中,键字段是 media_type

当我们定义这三种数据类型 TVMoviePerson 的类时,我们将对每个类类型使用一个简单的接口来表示集合。

public interface IDataType
{
}

在集合上使用自定义 JsonConverter JsonDataTypeConverter。它使用 "media_type" 属性识别对象类型,并生成相应的类并填充字段。

public class JsonDataTypeConverter : JsonConverter<List<IDataType>?>
{
    public override List<IDataType>? 
    Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartArray)
            return default;

        // polymorphic deserialization
        List<IDataType>? items = default;

        // walk through all of the items in the collection
        foreach (JsonObject jsonObject in 
                 JsonSerializer.Deserialize<List<JsonObject>>(ref reader, options)!)
        {
            // Deserialize based on media_type field
            IDataType? item = jsonObject["media_type"]?.GetValue<string>() switch
            {
                "tv" => jsonObject.Deserialize<TV>(options)!,
                "movie" => jsonObject.Deserialize<Movie>(options)!,
                "person" => jsonObject.Deserialize<Person>(options)!,
                _ => null
            };

            if (item is null) continue;

            items ??= new();
            items.Add(item);
        }

        return items;
    }

    public override void Write(Utf8JsonWriter writer, 
           List<IDataType>? value, JsonSerializerOptions options)
        => throw new NotImplementedException();
}

然后使用:

public class Response
{
    [JsonPropertyName("results"), JsonConverter(typeof(JsonDataTypeConverter))]
    public List<IDataType>? Results { get; set; }
}

下面是查看 IntelliSense 调试窗口时集合的屏幕截图,显示了包含多种对象类型的集合。

Google Drive

另一个例子是 Google Drive API。例如,File 类型有两个标识符:一个用于区分文件和文件夹,另一个用于区分 File 的类型 - Jpg、Png、文本文件、文档、MP4 等。这可以在 JSON 数据中看到。

{
 "kind": "drive#fileList",
 "incompleteSearch": false,
 "files": [
  {
   "kind": "drive#file",
   "mimeType": "video/mp4"
  },
  {
   "kind": "drive#file",
   "mimeType": "application/vnd.google-apps.folder"
  },
  {
   "kind": "drive#file",
   "mimeType": 
   "application/vnd.openxmlformats-officedocument.presentationml.presentation"
  },
  {
   "kind": "drive#file",
   "mimeType": 
   "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
  },
  {
   "kind": "drive#file",
   "mimeType": "text/plain"
  },
  {
   "kind": "drive#file",
   "mimeType": "image/png"
  }
 ]
}

上面的示例应用程序(WpfFileExplorer)的屏幕截图反映了不同的 File 类型。

上面的示例应用程序使用分层数据结构,并在打开每个分支时按需加载其数据。

所有不同类型的文件都具有相同的数据。因此,我们可以声明一个基类型并为所有不同类型的文件继承它。

[DebuggerDisplay("[File] {Name} ({Kind} | {MimeType})")]
public class File : IResourceKind
{
    [JsonPropertyName("kind")]
    public string? Kind { get; set; }

    [JsonPropertyName("mimeType")]
    public string? MimeType { get; set; }
}

[DebuggerDisplay("[FOLDER] {Name}")]
public class Folder : File
{
}

[DebuggerDisplay("[TXT DOCUMENT] {Name}")]
public class TxtDocument : File
{
}

[DebuggerDisplay("[EXCEL DOCUMENT] {Name}")]
public class ExcelDocument : File
{

}

[DebuggerDisplay("[PNG IMAGE] {Name}")]
public class PngImage : File
{
}

[DebuggerDisplay("[MP4 VIDEO] {Name}")]
public class Mp4Video : File
{
}

[DebuggerDisplay("[ZIPPED] {Name}")]
public class Zipped : File
{
}

旁注:在使用 XAML 中的隐式模板时,您可以定义一个默认模板,在这种情况下,是为基 File 类型定义的。如果找不到数据类型(类)的隐式模板,则为继承的基类类型应用基模板。

<DataTemplate DataType="{x:Type m:File}">
    <!-- Template here -->
</DataTemplate>

由于存在大量 File 类型的通用基类型,因此可以使用自定义 JsonConverter 内部的紧凑处理程序,例如 Google DriveJsonDataTypeConverter。在这里,我们将有一个用于“mime_type” JSON 字段的查找字典表,以及一个带有转换类型的引用方法。

public sealed class JsonDataTypeConverter : JsonConverter<List<File>?>
{
    public override List<File>? Read(ref Utf8JsonReader reader, 
                    Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartArray)
            return default;

        List<File> items = new();

        // walk through all of the items in the collection
        foreach (JsonObject jsonObject in 
                 JsonSerializer.Deserialize<List<JsonObject>>(ref reader, options)!)
        {
            // Deserialize based on kind field
            string? kind = jsonObject["kind"]?.GetValue<string>();

            if (string.IsNullOrEmpty(kind))
                continue;

            if (kind.Equals("drive#file"))
                ProcessFileType(jsonObject, items);
        }

        return items;
    }

    // polymorphic deserialization
    private void ProcessFileType(JsonObject jsonObject, List<File> items)
    {
        // Deserialize based on mimeType field
        string? mimeType = jsonObject["mimeType"]?.GetValue<string>();

        if (string.IsNullOrEmpty(mimeType))
            return;

        items.Add(MimeToFileConverter.Types[mimeType](jsonObject));
    }

    public override void Write(Utf8JsonWriter writer, 
                    List<File>? value, JsonSerializerOptions options)
        => throw new NotImplementedException();
}

支持的映射类:

public static class MimeToFileConverter
{
    // only some types are lists for briefity
    public static readonly Dictionary<string, Func<JsonObject, File>> Types = new()
    {
        { "application/vnd.google-apps.folder", Convert<Folder>()! },
        { "image/jpeg", Convert<JpgImage>()! },
        { "image/png", Convert<PngImage>()! },
        { "application/zip", Convert<Zipped>()! },
        { "application/x-zip-compressed", Convert<Zipped>()! },
        { "video/mp4", Convert<Mp4Video>()! },
        { "text/plain", Convert<TxtDocument>()! },
        { "application/vnd.openxmlformats-officedocument.presentationml.presentation", 
           Convert<PptDocument>()! },
        { "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 
           Convert<WordDocument>()! }
    };
    
    // Convert Json Object data into a specified class type
    private static Func<JsonObject, File?> Convert<TModel>() where TModel : File
        => jsonObject => JsonHelper.ToClass<TModel>(jsonObject.ToString(), 
           JsonSerializerConfiguration.Options);
}

File 集合属性设置的 IntelliSense 调试窗口正确反映了不同 File 类型的递归反序列化。

递归反序列化

JSON 对象结构可能有很多节点层级。每个节点可以有自己的自定义 JsonConverter 的属性。一个例子是上面MovieDB 中的多值类型集合部分。以下是 Person JSON 节点结构的提取,其中包含 known_for 节点集合:

{
    "page": 1,
    "total_results": 3433,
    "total_pages": 172,
  "results": [
    {
      "media_type": "tv",
    },
    {
      "media_type": "person",
      "known_for": [
        {
          "media_type": "movie",
        }
      ]
    },
    {
      "media_type": "movie",
    }
  ]
}

上面的 JsonDataTypeConverter 类已经支持递归反序列化,并将自动反序列化该结构。

Person 类中,我们应用 JsonConverter 属性 JsonDataTypeConverter

public class Person : RecordBase
{
    [JsonPropertyName("known_for"), JsonConverter(typeof(JsonDataTypeConverter))]
    public List<IDataType>? KnownFor { get; set; }
}

在这里,我们可以从 Person.KnownFor 集合属性的 IntelliSense 调试窗口看到,它正确地反映了 Movie 类类型的递归反序列化。

处理 JavaScript JSON 中无效的集合属性名称

JavaScript 不像 C# 语言那样严格,允许 C# 无效的属性名称。例如(来自 JavaScript Navigator 对象生成的 JSON 的摘录):

{
    ...
    "plugins": {
        "0": {
            "0": {},
            "1": {}
        },
        "1": {
            "0": {},
            "1": {}
        },
        "2": {
            "0": {},
            "1": {}
        },
        "3": {
            "0": {},
            "1": {}
        },
        "4": {
            "0": {},
            "1": {}
        }
    },
    ...
}

如您所见,我们有无效的属性名称,如 "0""1""2" 等。在 C# 中,属性名称必须以字母数字字符开头。由于这是一个集合,我们不能仅仅在多个属性上使用 JsonProperty 属性,因为我们不知道需要期望多少元素/属性。

如果您尝试在没有自定义 JsonConverter 的情况下反序列化上面的 JSON “plugins”对象,您会看到类似以下的错误:

标准的 JSON 集合用方括号括起来,看起来像这样:

{
    ....
    "languages": [
        "en-GB",
        "en",
        "en-US"
    ],
    ....
}

在自定义 JsonConverter 中处理“plugins”集合有多种方法。您可以选择使用 Dictionary<string, <Dictionary<string, Object>>List<Dictionary<string, Object>>List<List<object>>。对于本文,我选择了使用类来支持非标准属性命名。

public class UserAgentModel
{
    [JsonPropertyName("plugins"), JsonConverter(typeof(UserAgentPluginsConverter))]
    public List<PluginModel>? Plugins { get; set; }
}

public class PluginModel
{
    public string? Id { get; set; }
    public List<PluginObjectModel>? Values { get; set; }
}

public class PluginObjectModel
{
    public string? Id { get; set; }
    public object? Value { get; set; }
}

使用自定义 JsonConverter UserAgentPluginsConverter 将 JSON 映射到类结构。

public class UserAgentPluginsConverter : JsonConverter<List<PluginModel>?>
{
    public override List<PluginModel>? 
    Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            return default;

        List<PluginModel>? plugins = new();

        foreach (KeyValuePair<string, JsonNode?> 
        jsonObject in JsonSerializer.Deserialize<JsonObject>(ref reader, options)!)
            plugins.Add(new()
            {
                Id = jsonObject.Key,
                Values =  GetValues(jsonObject.Value)
            });

        return plugins;
    }

    private List<PluginObjectModel> GetValues(JsonNode? jsonNode)
    {
        List<PluginObjectModel> objects = new();

        foreach (KeyValuePair<string, JsonNode?> node in 
                 jsonNode.Deserialize<JsonObject>()!)
        {
            objects.Add(new()
            {
                Id = node.Key,
                Value = node.Value.Deserialize<object>()
            });
        }

        return objects;
    }

    public override void Write(Utf8JsonWriter writer, 
           List<PluginModel>? value, JsonSerializerOptions options)
        => throw new NotImplementedException();
}

现在我们的模型将如我们所愿。

最后,我们的示例应用程序可以显示数据(WpfUserAgent)。

数据转换

通常,在转换数据时,这是一个两步过程。首先,我们将 JSON 数据转换为 .NET 类(此情况下总共 34 个类),然后转换数据。

我最后演示的示例是如何将 JSON 数据一步转换为自定义类集合。通过这样做,我们将所需类的数量从 34 个减少到了 4 个!

最终结果如下所示(WpfApplicationRateLimitStatus)。

由于数据类太多无法在此处全部发布,我将只讨论使用的 JsonConverter。您可以查看并运行下载中包含的示例项目 WpfApplicationRateLimitStatus。我包含了标准和自定义映射类。

JsonApiRateLimitsConverter 将多个类压缩成与应用程序需求兼容的更简单的数据集合。

public sealed class JsonApiRateLimitsConverter : 
       JsonConverter<ObservableCollection<RateCategoryModel>?>
{
    public override ObservableCollection<RateCategoryModel>? 
    Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            return default;

        ObservableCollection<RateCategoryModel> items = new();

        foreach (KeyValuePair<string, JsonNode?> jsonObject in JsonSerializer
                     .Deserialize<JsonObject>(ref reader, options)!)

            if (ProcessChild(jsonObject, options) is { } rate)
                items.Add(rate);

        return items.Count > 0
            ? items
            : default;
    }

    private RateCategoryModel ProcessChild
    (KeyValuePair<string, JsonNode?> jsonObject, JsonSerializerOptions options)
    {
        RateCategoryModel rate = new() { Name = jsonObject.Key };

        foreach (KeyValuePair<string, JsonNode?> child in 
                 jsonObject.Value.Deserialize<JsonObject>(options)!)
        {
            ApiRateLimitModel? limit = 
               child.Value.Deserialize<ApiRateLimitModel>(options);

            if (limit is null)
                continue;

            rate.Limits.Add(new()
            {
                Name = GetPropertyName(child.Key),
                Limit = limit
            });
        }

        return rate;
    }

    private static string GetPropertyName(string value)
    {
        string name = "__no_name__";

        if (!string.IsNullOrEmpty(value))
        {
            string[] parts = value.Split(new[] { '/' }, 
                             StringSplitOptions.RemoveEmptyEntries);
            name = string.Join("_", parts.Skip(1)).Replace(":", "");

            if (string.IsNullOrEmpty(name))
                name = parts[0];
        }

        return name;
    }
    
    public override void Write(Utf8JsonWriter writer, 
    ObservableCollection<RateCategoryModel>? value, JsonSerializerOptions options)
        => throw new NotImplementedException();
}

然后使用:

public class APIRateStatusModel
{
    [JsonPropertyName("resources"), JsonConverter(typeof(JsonApiRateLimitsConverter))]
    public ObservableCollection<RateCategoryModel>? Resources { get; set; }
}
private void GetData(string buttonName)
{
    // Retrieve JSON data from file
    string rawJson = File.ReadAllText(Path.Combine(filePath, fileName));

    // Convert to C# Class List of typed objects
    Result = JsonHelper.ToClass<APIRateStatusModel>(rawJson);
}

摘要

本文不仅展示了如何使用 System.Text.Json API,还展示了链接的前一篇文章中 NewtonSoft.Json 代码的迁移。

示例应用程序

下载中包含七 (7) 个 DotNet (Core) 示例应用程序。

  1. WinFormSimpleObject - Etsy Category
    • WinForm,代码隐藏
  2. WinFormSimpleCollection - Etsy Categories
    • WinForm,数据绑定,MVVM(简单)
  3. WpfPhoto - Flickr Photo Viewer
    • WPF,MVVM
    • JsonFlickrCollectionConverterJsonFlickrContentConverterJsonFlickrUnixDateContentConverterJsonFlickrUriContentConverterStringEnumConverter
  4. WpfMultiSearch - MovieDB MultiSearch 结果
    • WPF,MVVM,隐式模板
    • JsonDataTypeConverterJsonPartialUrlConverter
  5. WpfApplicationRateLimitStatus - Twitter JSON 数据转换
    • WPF,MVVM,DataGrid
    • JsonUnixDateConverterJsonApiRateLimitsConverter
  6. WpfFileExplorer - Google Drive 文件浏览器
    • WPF,MVVM,具有按需加载和自定义模板的 TreeView,隐式模板,分层数据绑定
    • JsonDataTypeConverterJsonGoogleUriContentConverter
  7. WpfUserAgent - 处理 JavaScript JSON 中无效的集合属性名称
    • WPF,MVVM,具有隐式模板的 TreeView,分层数据绑定
    • UserAgentPluginsConverter

所有示例的下载链接上方提供。

历史

© . All rights reserved.