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

使用 C# & VB 中的 Newtonsoft.Json

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (96投票s)

2017 年 8 月 15 日

CPOL

12分钟阅读

viewsIcon

394428

downloadIcon

15893

处理简单的 JSON 对象和集合,到自定义转换器和 .NET 类的转换 - NewtonSoft.Json

使用 JSON 系列

下载

引言

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

如果您不熟悉 JSON,以下是来自官方 http://www.json.org 的定义

引用

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

目录

背景

为什么还要写一篇关于 JSON 的文章?本文的灵感来自 CodeProject 网站上的 快速问答 部分中提出的许多问题。我们将使用一系列从基本到高级的真实示例,来自 EtsyFlickrMovieDBGoogle DriveTwitter,以及涵盖从简单对象序列化到自定义转换器和数据转换的解决方案。

工具与库

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

查看器与验证器

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

代码生成器

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

  • JSON Utils - 同时支持 VB 和 C#,并提供许多选项
  • Visual Studio 插件: Json2Csharp - 将剪贴板上的 JSON 数据转换为 C# 类
  • quicktype.io - 支持 C#、TypeScript、Go、Java、Elm、Swift、简单类型和 Schema

序列化库

最后,您需要将原始 JSON 数据映射到类结构。同样,市面上也有一些库可供选择:

对于简单的项目,所有这三个库都能满足 99% ~ 100% 的需求。对于更中级和高级的工作,例如自定义数据转换器和转换,我们需要 Newtonsoft 的 Json.NET 库。这一点在本篇文章的后面会更加明显。

数据转换

一旦您有了原始 JSON 数据并创建了要映射数据的类,下一步就是将其反序列化为类,并从类序列化。本文将重点介绍反序列化。以下辅助类简化了此任务。

using Newtonsoft.Json;
using System.Collections.Generic;

namespace Support.CSharp
{
    public static class JsonHelper
    {
        public static string FromClass<T>(T data, bool isEmptyToNull = false,
                                          JsonSerializerSettings jsonSettings = null)
        {
            string response = string.Empty;

            if (!EqualityComparer<T>.Default.Equals(data, default(T)))
                response = JsonConvert.SerializeObject(data, jsonSettings);

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

        public static T ToClass<T>
        (string data, JsonSerializerSettings jsonSettings = null)
        {
            var response = default(T);

            if (!string.IsNullOrEmpty(data))
                response = jsonSettings == null
                    ? JsonConvert.DeserializeObject<T>(data)
                    : JsonConvert.DeserializeObject<T>(data, jsonSettings);

            return response;
        }
    }
}
Imports Newtonsoft.Json

Public Module JsonHelper

    Public Function FromClass(Of T)(data As T,
                                    Optional isEmptyToNull As Boolean = False,
                                    Optional jsonSettings _
                                    As JsonSerializerSettings = Nothing) _
                                                          As String

        Dim response As String = String.Empty

        If Not EqualityComparer(Of T).Default.Equals(data, Nothing) Then
            response = JsonConvert.SerializeObject(data, jsonSettings)
        End If

        Return If(isEmptyToNull, (If(response = "{}", "null", response)), response)

    End Function

    Public Function ToClass(Of T)(data As String,
                                  Optional jsonSettings _
                                  As JsonSerializerSettings = Nothing) _
                                                        As T

        Dim response = Nothing

        If Not String.IsNullOrEmpty(data) Then
            response = If(jsonSettings Is Nothing,
                JsonConvert.DeserializeObject(Of T)(data),
                JsonConvert.DeserializeObject(Of T)(data, jsonSettings))
        End If

        Return response

    End Function

End Module

我们不会使用 JsonSerializerSettings。您可以在 Newtonsoft Json.NET 文档 中阅读更多关于它们的信息。

标准数据类型

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

简单对象类型

这是来自 Etsy API 的一个 Category 对象。所有 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
}

我们需要生成类来映射 JSON 数据。要做到这一点,我们将使用 JSON Utils

在这里,我们选择了 Pascal 命名约定。JSON 字段都是小写的。使用 Json.NETJsonProperty 属性,将 JSON 字段映射到 .NET 类属性非常简单。这是生成的类:

using Newtonsoft.Json;

namespace WinFormSimpleObject.Models
{
    public class Category
    {
        [JsonProperty("category_id")]
        public int CategoryId { get; set; }

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

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

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

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

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

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

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

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

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

        [JsonProperty("num_children")]
        public int NumChildren { get; set; }
    }
}
Imports Newtonsoft.Json

Namespace Models

    Public Class Category

        <JsonProperty("category_id")>
        Public Property CategoryId As Integer

        <JsonProperty("name")>
        Public Property Name As String

        <JsonProperty("meta_title")>
        Public Property MetaTitle As String

        <JsonProperty("meta_keywords")>
        Public Property MetaKeywords As String

        <JsonProperty("meta_description")>
        Public Property MetaDescription As String

        <JsonProperty("page_description")>
        Public Property PageDescription As String

        <JsonProperty("page_title")>
        Public Property PageTitle As String

        <JsonProperty("category_name")>
        Public Property CategoryName As String

        <JsonProperty("short_name")>
        Public Property ShortName As String

        <JsonProperty("long_name")>
        Public Property LongName As String

        <JsonProperty("num_children")>
        Public Property NumChildren As Integer

    End Class

End Namespace

现在我们可以将 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);
}
Public Property Result() As Category

Private Const fileName As String = "Etsy_Category.Json"
Private ReadOnly filePath As String = Environment.CurrentDirectory

Private Sub GetData()

    ' Retrieve JSON data from file
    Dim rawJson = File.ReadAllText(Path.Combine(filePath, fileName))

    ' Convert to C# Class typed object
    Result = JsonHelper.ToClass(Of Category)(rawJson)

End Sub

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

简单集合类型

与许多其他 API 一样,Etsy 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
{
    [JsonProperty("effective_limit")]
    public int? EffectiveLimit { get; set; }

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

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

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

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

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

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

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

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

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

    [JsonProperty("pagination")]
    public Pagination Pagination { get; set; }
}
Public Class Pagination

    <JsonProperty("effective_limit")>
    Public Property EffectiveLimit() As Integer?

    <JsonProperty("effective_offset")>
    Public Property EffectiveOffset() As Integer?

    <JsonProperty("effective_page")>
    Public Property EffectivePage() As Integer?

    <JsonProperty("next_offset")>
    Public Property NextOffset() As Integer?

    <JsonProperty("next_page")>
    Public Property NextPage() As Integer?

End Class

Public Class Params

    <JsonProperty("tag")>
    Public Property Tag As String

End Class

Public Class Response(Of TModel As Class)

    <JsonProperty("count")>
    Public Property Count As Integer

    <JsonProperty("results")>
    Public Property Results As IList(Of TModel)

    <JsonProperty("params")>
    Public Property Params As Params

    <JsonProperty("type")>
    Public Property Type As String

    <JsonProperty("pagination")>
    Public Property Pagination As Pagination

End Class

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

public BindingList<Category> Categories { get; set; }

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

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

    // Get collection of objects
    if (response != null && response.Results != null && response.Results.Count > 0)
    {
        var data = response.Results;
        Categories.Clear();
        for (int i = 0; i < data.Count; i++)
        {
            Categories.Add(data[i]);
        }
    }
}
Public Property Categories() As BindingList(Of Category)

Private Sub GetData()

    ' Retrieve JSON data from file
    Dim rawJson = File.ReadAllText(Path.Combine(filePath, fileName))

    ' Convert to C# Class typed object
    Dim response = JsonHelper.ToClass(Of Response(Of Category))(rawJson)

    ' Get collection of objects
    If response IsNot Nothing _
        AndAlso response.Results IsNot Nothing _
        AndAlso response.Results.Count > 0 Then

        Dim data = response.Results
        Categories.Clear()
        For i As Integer = 0 To data.Count - 1
            Categories.Add(data(i))
        Next

    End If

End Sub

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

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

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

UNIX 纪元时间戳

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

引用

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

这是来自 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
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
                    object existingValue, JsonSerializer serializer)
    {
        double value = 0;
        if (double.TryParse(Convert.ToString(reader.Value), out value))
            return value.FromUnixDate();

        if (objectType == typeof(DateTime))
            return default(DateTime);

        return null;
    }

    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
Friend Module Unix

    Friend ReadOnly Epoch As New DateTime(year:=1970, month:=1, day:=1, 
           hour:=0, minute:=0, second:=0, millisecond:=0, kind:=DateTimeKind.Utc)

End Module

Public Module DoubleExtensions

    <Extension>
    Public Function FromUnixDate(unixDate As Double?) As DateTime
        Return Epoch.AddSeconds(If(unixDate, 0.0))
    End Function

    <Extension>
    Public Function FromUnixDate(unixDate As Double) As DateTime
        Return Epoch.AddSeconds(unixDate)
    End Function

End Module

Public NotInheritable Class JsonUnixDateConverter : Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType Is GetType(DateTime) OrElse objectType Is GetType(Date?)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, 
           existingValue As Object, serializer As JsonSerializer) As Object

        Dim value As Double = 0
        If Double.TryParse(Convert.ToString(reader.Value), value) Then
            Return value.FromUnixDate()
        End If

        If objectType Is GetType(DateTime) Then
            Return Nothing
        End If

        Return Nothing

    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, 
                                   serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

工作原理

JsonUnixDateConverter.CanConvert 检查是否是正确的数据类型。如果匹配,则执行 JsonUnixDateConverter.ReadJson,解析该值,将其从 UNIX 纪元转换为 .NET Date 类型,并返回该值以分配给类属性。

如何使用

只需将 JsonUnixDateConverter 应用到属性上:

[JsonProperty("lastupdate"), JsonConverter(typeof(JsonUnixDateConverter))]
public DateTime? LastUpdate { get; set; }
<JsonProperty("lastupdate"), JsonConverter(GetType(JsonUnixDateConverter))>
Public Property LastUpdate() As Date?

数据结构类型

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

Flickr 有很多这样的例子,其中 XML 不直接翻译 - XML 既有属性也有描述数据的元素,而 JSON 只有字段。例如,Photo 对象和评论计数字段。

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

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

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

public class Photo
{
    [JsonProperty("comments")]
    public Comments Comments { get; set; }
}
public class Comments
{
    [JsonProperty("_content")]
    public string Content { get; set; }
}

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

然后使用上面的类结构:

int GetCommentCount(Photo photo)
{
    return photo.Comments.Count;
}
Private Function GetCommentCount(photo As Photo) As Integer
    Return photo.Comments.Count
End Function

如果我们能将评论计数简化为单个整数而不是一个类对象,那会好得多。

int GetCommentCount(Photo photo)
{
    return photo.CommentCount;
}
Private Function GetCommentCount(photo As Photo) As Integer
    Return photo.CommentCount
End Function

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

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

解决方案是使用通用的 JsonConverter

public sealed class JsonFlickrContentConverter<TModel> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(TModel);
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
                    object existingValue, JsonSerializer serializer)
    {
        var result = default(TModel);

        switch (reader.TokenType)
        {
            case JsonToken.Integer:
            case JsonToken.Float:
            case JsonToken.String:
            case JsonToken.Boolean:
            case JsonToken.Date:
                // convert from string or type to correct type
                // eg: "isfavorite": 0 to bool? = false
                result = (TModel)Convert.ChangeType(reader.Value, GetUnderlyingType());
                break;
            case JsonToken.StartObject:
                // sub-node _content holds actual value
                //  eg: "comments": { "_content": "483" } to int? = 483
                GetObject(reader, out result);
                break;
        }
        return result;
    }

    private void GetObject(JsonReader reader, out TModel result)
    {
        var tags = JObject.Load(reader).FindTokens("_content");
        result = (tags != null && tags.Count > 0)
            ? (TModel)Convert.ChangeType((string)tags[0], GetUnderlyingType())
            : result = default(TModel);
    }

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

    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
Public NotInheritable Class JsonFlickrContentConverter(Of TModel) : _
                                               Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(TModel)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, _
    objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object

        Dim result = Nothing

        Select Case reader.TokenType
            Case JsonToken.[Integer],
                 JsonToken.Float,
                 JsonToken.[String],
                 JsonToken.[Boolean],
                 JsonToken.[Date]
                ' convert from string or type to correct type
                ' eg: "isfavorite": 0 to bool? = false
                result = DirectCast(Convert.ChangeType_
                         (reader.Value, GetUnderlyingType()), TModel)
                Exit Select
            Case JsonToken.StartObject
                ' sub-node _content holds actual value
                '  eg: "comments": { "_content": "483" } to int? = 483
                GetObject(reader, result)
                Exit Select
        End Select

        Return result

    End Function

    Private Sub GetObject(reader As JsonReader, ByRef result As TModel)

        Dim tags = JObject.Load(reader).FindTokens("_content")

        result = If((tags IsNot Nothing AndAlso tags.Count > 0),
            DirectCast(Convert.ChangeType(CType(tags(0), String), _
                       GetUnderlyingType()), TModel),
            Nothing)

    End Sub

    ' converts Generic nullable type to underlying type
    ' eg: int? to int
    Private Function GetUnderlyingType() As Type
        Dim type = GetType(TModel)
        Return If(Nullable.GetUnderlyingType(type), type)
    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, _
           value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

然后使用:

public class Photo
{
    [JsonProperty("comments"), JsonConverter(typeof(JsonFlickrContentConverter<int?>))]
    public int? Comments { get; set; }
}
Public Class Photo
    <JsonProperty("comments"), _
     JsonConverter(GetType(JsonFlickrContentConverter(Of Integer?)))>
    Public Property Comments() As Integer?
End Class

展平集合类型

与数据结构类型类似,对象集合有时也无法很好地从 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
{
    [JsonProperty("notes")]
    public Notes Notes { get; set; }
}

public class Notes
{
    [JsonProperty("note")]
    public IList<Note> Note { get; set; }
}
Public Class Photo

    <JsonProperty("notes")>
    Public Property Notes As Notes

End Class

Public Class Notes

    <JsonProperty("note")>
    Public Property Note As IList(Of Note)

End Class

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

以下通用 JsonConverter 将其压缩,为 Photo 类返回一个 List<TModel>

public sealed class JsonFlickrCollectionConverter<TModel> : 
                    JsonConverter where TModel : class
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(TModel);
    }

    public override object ReadJson(JsonReader reader, _
    Type objectType, object existingValue, JsonSerializer serializer)
    {
        // A collection type containing only a collection of type
        // eg: "urls": { "url": [{...}, {...}, ...] }
        if (reader.TokenType == JsonToken.StartObject)
        {
            var jObj = JObject.Load(reader);
            if (jObj.HasValues)
            {
                var items = new List<TModel>();
                foreach (var value in jObj.Values())
                {
                    try
                    {
                        items.AddRange_
                        (JsonHelper.ToClass<IList<TModel>>(value.ToString()));
                    }
                    catch (Exception)
                    {
                        // unexpected type
                        return null;
                    }
                }
                return items;
            }
        }
        return null;
    }

    public override void WriteJson(JsonWriter writer, 
                    object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
Public NotInheritable Class JsonFlickrCollectionConverter(Of TModel As Class) : _
                      Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(TModel)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, _
    objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object

        ' A collection type containing only a collection of type
        ' eg: "urls": { "url": [{...}, {...}, ...] }
        If reader.TokenType = JsonToken.StartObject Then

            Dim jObj = JObject.Load(reader)

            If jObj.HasValues Then

                Dim items = New List(Of TModel)

                For Each value In jObj.Values()
                    Try
                        items.AddRange(JsonHelper.ToClass_
                        (Of IList(Of TModel))(value.ToString()))
                    Catch generatedExceptionName As Exception
                        ' unexpected type
                        Return Nothing
                    End Try
                Next

                Return items

            End If

        End If

        Return Nothing

    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, _
           value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

然后使用:

[JsonProperty("notes"), JsonConverter(typeof(JsonFlickrCollectionConverter<Note>))]
public IList<Note> Notes { get; set; }
<JsonProperty("notes"), JsonConverter(GetType(JsonFlickrCollectionConverter(Of Note)))>
Public Property Notes() As IList(Of Note)

多值类型集合

这是指返回的对象集合属于相似或不同的复杂对象类型。themoviedb.orgMulti-search 在同一个集合中返回 TVMoviePerson 类型。Google Drive 是另一个返回包含 文件和文件夹 复杂对象类型的集合的例子。

MovieDB.Org

这是随附的 MovieDB 演示应用程序的屏幕截图,其中显示了此示例:

上面屏幕截图的 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
{
}
Public Interface IDataType
End Interface

使用自定义 JsonConverter JsonDataTypeConverter 应用于集合。它会识别对象类型,生成相应的类并填充字段。

public sealed class JsonDataTypeConverter : JsonConverter
{
    // destination data type
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(IDataType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
                                    object existingValue, JsonSerializer serializer)
    {
        List<IDataType> results = null;

        // Are there multiple results?
        if (reader.TokenType== JsonToken.StartArray)
        {
            // Retrieve a list of Json objects
            var jObjs = serializer.Deserialize<IList<JObject>>(reader);
            if (jObjs != null && jObjs.Count > 0)
            {
                results = new List<IDataType>();

                for (int i = 0; i < jObjs.Count; i++)
                {
                    // Does the object contain a "media_type" 
                    // identifier - eg: "media_type":
                    var token = jObjs[i].FindTokens("media_type");
                    if (token != null && token.Count > 0)
                    {
                        // Only one is expected
                        switch (token[0].ToString())
                        {
                            // "media_type": "tv"
                            case "tv":
                                results.Add(Convert<TV>(jObjs[i]));
                                break;
                            // "media_type": "movie"
                            case "movie":
                                results.Add(Convert<Movie>(jObjs[i]));
                                break;
                            // "media_type": "person"
                            case "person":
                                results.Add(Convert<Person>(jObjs[i]));
                                break;
                        }
                    }
                }
            }
        }
        return results;
    }

    // Convert Json Object data into a specified class type
    private TModel Convert<TModel>(JObject jObj) where TModel : IDataType
    {
        return JsonHelper.ToClass<TModel>(jObj.ToString());
    }

    // one way conversion, so back to Json is not required
    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
Public NotInheritable Class JsonDataTypeConverter : Inherits JsonConverter

    ' destination data type
    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(IDataType)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, _
                     existingValue As Object, serializer As JsonSerializer) As Object

        Dim results As List(Of IDataType) = Nothing

        ' Are there multiple results?
        If reader.TokenType = JsonToken.StartArray Then

            ' Retrieve a list of Json objects
            Dim jObjs = serializer.Deserialize(Of IList(Of JObject))(reader)
            If jObjs IsNot Nothing AndAlso jObjs.Count > 0 Then
                results = New List(Of IDataType)()

                For i As Integer = 0 To jObjs.Count - 1

                    ' Does the object contain a "media_type" identifier - 
                    ' e.g.: "media_type":
                    Dim token = jObjs(i).FindTokens("media_type")
                    If token IsNot Nothing AndAlso token.Count > 0 Then

                        ' Only one is expected
                        Select Case token(0).ToString()
                            ' "media_type": "tv"
                            Case "tv"
                                results.Add(Convert(Of TV)(jObjs(i)))
                                Exit Select
                            ' "media_type": "movie"
                            Case "movie"
                                results.Add(Convert(Of Movie)(jObjs(i)))
                                Exit Select
                            ' "media_type": "person"
                            Case "person"
                                results.Add(Convert(Of Person)(jObjs(i)))
                                Exit Select
                        End Select

                    End If
                Next
            End If
        End If

        Return results

    End Function

    ' Convert Json Object data into a specified class type
    Private Function Convert(Of TModel As IDataType)(jObj As JObject) As TModel
        Return JsonHelper.ToClass(Of TModel)(jObj.ToString())
    End Function

    ' one way conversion, so back to Json is not required
    Public Overrides Sub WriteJson(writer As JsonWriter, _
                                   value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

然后使用:

public class Response
{
    [JsonProperty("results"), JsonConverter(typeof(JsonDataTypeConverter))]
    public IList<IDataType> Results { get; set; }
}
Public Class Response

    <JsonProperty("results"), JsonConverter(GetType(JsonDataTypeConverter))>
    Public Property Results() As IList(Of IDataType)

End Class

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

Google Drive

另一个例子是 Google Drive API。例如,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"
  }
 ]
}

不同的 文件 类型可以在下面的示例应用程序屏幕截图中看到:

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

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

public class File : IResourceKind
{
    [JsonProperty("kind")]
    public string Kind { get; set; }

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

public class Folder : File
{
}

public class TxtDocument : File
{
}

public class WordDocument : File
{
}

public class PngImage : File
{
}

public class JpgImage : File
{
}

public class Zipped : File
{
}
Public Class File : Implements IResourceKind

    <JsonProperty("kind")>
    Public Property Kind As String

    <JsonProperty("mimeType")>
    Public Property MimeType As String

End Class

Public Class Folder : Inherits File
End Class

Public Class TxtDocument : Inherits File
End Class

Public Class WordDocument : Inherits File
End Class

Public Class PngImage : Inherits File
End Class

旁注 在使用 XAML 中的隐式模板时,您可以定义一个默认模板,在本例中是基本的 File 类型,如果没有找到该数据类型(类)的隐式模板,则为继承的基类类型应用基本模板。

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

由于大量 文件 类型有一个共同的基类型,因此可以在自定义 JsonConverter 中使用一个紧凑的处理程序,对于 Google Drive,可以将其命名为 JsonDataTypeConverter。这里,我们将有一个查找字典表来处理 "mime_type" JSON 字段以及方法引用和转换类型。

// only some types are lists for brevity
private readonly Dictionary<string, Func<JObject, File>> mimeTypes
    = new Dictionary<string, Func<JObject, File>>
{
    { "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 file of type.... mimeTypes
private void ProcessFileType(List<File> results, IList<JObject> jObjs, int i)
{
    var fileToken = jObjs[i].FindTokens("mimeType");
    if (fileToken != null && fileToken.Count > 0)
    {
        var key = mimeTypes.Keys.FirstOrDefault(x => x.Equals(fileToken[0].ToString()));
        if (key != null)
        {
            results.Add(mimeTypes[key](jObjs[i]));
        }
    }
}

// Convert Json Object data into a specified class type
private static Func<JObject, File> Convert<TModel>() where TModel : File
{
    return (jObj) => JsonHelper.ToClass<TModel>(jObj.ToString());
}
' only some types are lists for brevity
Private ReadOnly mimeTypes As New Dictionary(Of String, Func(Of JObject, File))() From {
    {"application/vnd.google-apps.folder", Convert(Of Folder)()},
    {"image/jpeg", Convert(Of JpgImage)()},
    {"image/png", Convert(Of PngImage)()},
    {"application/zip", Convert(Of Zipped)()},
    {"application/x-zip-compressed", Convert(Of Zipped)()},
    {"video/mp4", Convert(Of Mp4Video)()},
    {"text/plain", Convert(Of TxtDocument)()},
    {"application/vnd.openxmlformats-officedocument.presentationml.presentation",
     Convert(Of PptDocument)()},
    {"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
     Convert(Of WordDocument)()}
}

' Convert file of type.... mimeTypes
Private Sub ProcessFileType(results As List(Of File), _
                            jObjs As IList(Of JObject), i As Integer)

    Dim fileToken = jObjs(i).FindTokens("mimeType")

    If fileToken IsNot Nothing AndAlso fileToken.Count > 0 Then

        Dim key = mimeTypes.Keys.FirstOrDefault(Function(x) _
                  x.Equals(fileToken(0).ToString()))

        If key IsNot Nothing Then
            results.Add(mimeTypes(key)(jObjs(i)))
        End If

    End If

End Sub

' Convert Json Object data into a specified class type
Private Shared Function Convert(Of TModel As File)() As Func(Of JObject, File)
    Return Function(jObj) JsonHelper.ToClass(Of TModel)(jObj.ToString())
End Function

对于 文件 集合属性,IntelliSense 调试窗口正确反映了不同 文件 类型的递归反序列化:

递归反序列化

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 类已经支持递归反序列化。一旦识别出节点对象类型,我们就调用 JsonHelper.ToClass<...>(...) 方法:

// Convert Json Object data into a specified class type
private TModel Convert<TModel>(JObject jObj) where TModel : IDataType
{
    return JsonHelper.ToClass<TModel>(jObj.ToString());
}
' Convert Json Object data into a specified class type
Private Function Convert(Of TModel As IDataType)(jObj As JObject) As TModel
    Return JsonHelper.ToClass(Of TModel)(jObj.ToString())
End Function

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

public class Person : RecordBase
{
    [JsonProperty("known_for"), JsonConverter(typeof(JsonDataTypeConverter))]
    public IList<IDataType> KnownFor { get; set; }
}
Public Class Person : Inherits RecordBase

    <JsonProperty("known_for"), JsonConverter(GetType(JsonDataTypeConverter))>
    Public Property KnownFor As IList(Of IDataType)

End Class

在这里,我们可以从 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
{
    // trimmed for brevity

    [JsonProperty("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; }
}
Public Class UserAgentModel

    ' trimmed for brevity

    <JsonProperty("plugins")>
    <JsonConverter(GetType(UserAgentPluginsConverter))>
    Public Property Plugins As List(Of PluginModel)

End Class

Public Class PluginModel

    Public Property Id As String
    Public Property Values As List(Of PluginObjectModel)

End Class

Public Class PluginObjectModel

    Public Property Id As String
    Public Property Value As Object

End Class

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

public class UserAgentPluginsConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(PluginModel);
    }
 
    public override object ReadJson(JsonReader reader, Type objectType,
                                    object existingValue, JsonSerializer serializer)
    {
        List<PluginModel> result = new List<PluginModel>();
 
        // Is this the start of an object?
        if (reader.TokenType == JsonToken.StartObject)
        {
            var jObj = JObject.Load(reader);
            if (jObj.HasValues)
            {
                // Step through each top property node
                foreach (var child in jObj.Children())
                {
                    result.Add(new PluginModel
                    {
                        Id = child.Path,
                        Values =  ProcessChild(child, serializer)
                    });
                } 
            }
        }
        
        return result;
    }
 
    private List<PluginObjectModel> ProcessChild(JToken child, JsonSerializer serializer)
    {
        var objects = new List<PluginObjectModel>();
        var node = serializer.Deserialize<JProperty>(child.CreateReader());
 
        if (node.HasValues)
        {
            foreach (var jToken in node.Values())
            {
                var item = serializer.Deserialize<JProperty>(jToken.CreateReader());
                objects.Add(new PluginObjectModel
                {
                    Id = item.Name,
                    Value = item.Value.ToString()
                });
            } 
        }
 
        return objects;
    }
 
    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
Public Class UserAgentPluginsConverter : Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(PluginModel)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, _
                                       existingValue As Object, _
                                       serializer As JsonSerializer) As Object

        Dim result = New List(Of PluginModel)()

        If reader.TokenType = JsonToken.StartObject Then
            Dim jObj = JObject.Load(reader)

            If jObj.HasValues Then

                For Each child In jObj.Children()
                    result.Add(New PluginModel With {
                        .Id = child.Path,
                        .Values = ProcessChild(child, serializer)
                    })
                Next
            End If
        End If

        Return result
    End Function

    Private Function ProcessChild(child As JToken, serializer As JsonSerializer) _
                                  As List(Of PluginObjectModel)

        Dim objects = New List(Of PluginObjectModel)()
        Dim node = serializer.Deserialize(Of JProperty)(child.CreateReader())

        If node.HasValues Then

            objects.AddRange(From jToken In node.Values()
                             Select item = serializer.Deserialize_
                             (Of JProperty)(jToken.CreateReader())
                             Select New PluginObjectModel With
                                {
                                    .Id = item.Name,
                                    .Value = item.Value.ToString()
                                })
        End If

        Return objects
    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, _
                     value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

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

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

数据转换

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

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

最终结果将是这样的:

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

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

public sealed class JsonApiRateLimitsConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(RateCategoryModel);
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
                    object existingValue, JsonSerializer serializer)
    {
        ObservableCollection<RateCategoryModel> result = null;

        // is this the start of an object?
        if (reader.TokenType == JsonToken.StartObject)
        {
            var jObj = JObject.Load(reader);
            if (jObj.HasValues)
            {
                // Step through each top property node
                result = new ObservableCollection<RateCategoryModel>();
                foreach (var child in jObj.Children())
                {
                    var rate = ProcessChild(child, serializer);
                    if (rate != null) result.Add(rate);
                }
            }
        }
        return result;
    }

    private static RateCategoryModel ProcessChild(JToken child, JsonSerializer serializer)
    {
        RateCategoryModel rate = null;
        var node = serializer.Deserialize<JProperty>(child.CreateReader());
        if (node.HasValues)
        {
            // step through each sub-property node
            rate = new RateCategoryModel { Name = node.Name };
            foreach (var item in node.Values())
            {
                var limit = serializer.Deserialize<JProperty>(item.CreateReader());
                rate.Limits.Add(new RateLimitModel
                {
                    // we only want the last part of the property name
                    Name = GetPropertyName(limit),
                    Limit = GetRateLimit(limit)
                });
            }
        }
        return rate;
    }

    private static string GetPropertyName(JProperty prop)
    {
        string name = string.Empty;

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

        return name;
    }

    private static ApiRateLimitModel GetRateLimit(JProperty limit)
    {
        ApiRateLimitModel model = null;

        if (!string.IsNullOrEmpty(limit.First.ToString()))
        {
            var rawJson = limit.First.ToString();
            model = JsonHelper.ToClass<ApiRateLimitModel>(rawJson);
        }

        return model;
    }

    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
Public NotInheritable Class JsonApiRateLimitsConverter : Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(RateCategoryModel)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, _
      objectType As Type, existingValue As Object, 
                          serializer As JsonSerializer) As Object

        Dim result As ObservableCollection(Of RateCategoryModel) = Nothing

        ' is this the start of an object?
        If reader.TokenType = JsonToken.StartObject Then

            Dim jObj = JObject.Load(reader)
            If jObj.HasValues Then

                ' Step through each top property node
                result = New ObservableCollection(Of RateCategoryModel)()
                For Each child In jObj.Children()

                    Dim rate = ProcessChild(child, serializer)

                    If rate IsNot Nothing Then
                        result.Add(rate)
                    End If

                Next

            End If
        End If

        Return result

    End Function

    Private Shared Function ProcessChild(child As JToken, _
            serializer As JsonSerializer) As RateCategoryModel

        Dim rate As RateCategoryModel = Nothing
        Dim node = serializer.Deserialize(Of JProperty)(child.CreateReader())

        If node.HasValues Then

            ' step through each sub-property node
            rate = New RateCategoryModel() With {.Name = node.Name}

            For Each item In node.Values()

                Dim limit = serializer.Deserialize(Of JProperty)(item.CreateReader())

                ' we only want the last part of the property name
                rate.Limits.Add(New RateLimitModel() With
                {
                    .Name = GetPropertyName(limit),
                    .Limit = GetRateLimit(limit)
                })

            Next

        End If

        Return rate

    End Function

    Private Shared Function GetPropertyName(prop As JProperty) As String

        Dim name As String = String.Empty

        If Not String.IsNullOrEmpty(prop.Name) Then

            Dim parts = prop.Name.Split(New Char() _
                        {"/"c}, StringSplitOptions.RemoveEmptyEntries)
            name = String.Join("_", parts.Skip(1)).Replace(":", "")

            If String.IsNullOrEmpty(name) Then
                name = prop.Name.Replace("/", "")
            End If

        Else
            name = "__noname__"
        End If

        Return name

    End Function

    Private Shared Function GetRateLimit(limit As JProperty) As ApiRateLimitModel

        Dim model As ApiRateLimitModel = Nothing

        If Not String.IsNullOrEmpty(limit.First.ToString()) Then
            Dim rawJson = limit.First.ToString()
            model = JsonHelper.ToClass(Of ApiRateLimitModel)(rawJson)
        End If

        Return model

    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, _
           value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

然后使用:

public class APIRateStatusModel
{
    [JsonProperty("resources"), JsonConverter(typeof(JsonApiRateLimitsConverter))]
    public ObservableCollection<RateCategoryModel> Resources { get; set; }
}
Public Class APIRateStatusModel

    <JsonProperty("resources"), JsonConverter(GetType(JsonApiRateLimitsConverter))>
    Public Property Resources() As ObservableCollection(Of RateCategoryModel)

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

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

    ' Retrieve JSON data from file
    Dim rawJson = File.ReadAllText(Path.Combine(filePath, fileName))

    ' Convert to C# Class List of typed objects
    Result = JsonHelper.ToClass(Of APIRateStatusModel)(rawJson)

End Sub

总结

我们已经介绍了如何处理标准值类型、自定义值类型、将结构压缩到类型,以及如何从原始 JSON 结构转换为支持应用程序需求的类结构。Newtonsoft 的 Json.NET、辅助类和自定义 JsonConverters 使处理 JSON 数据变得干净且非常容易。

示例应用程序

下载中包含七 (7) 个示例应用程序,包括 C# 和 VB 两个版本:

  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 File Explorer
    • WPF,MVVM,带按需加载和自定义模板的 TreeView,隐式模板,分层数据绑定
    • JsonDataTypeConverterJsonGoogleUriContentConverter
  7. WpfUserAgent - 处理 Javascript JSON 文件中的无效集合属性名
    • WPF,MVVM,带隐式模板的 TreeView,分层数据绑定
    • UserAgentPluginsConverter

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

历史

  • v1.0 - 2017 年 8 月 15 日 - 初始发布
  • v1.1 - 2017 年 8 月 16 日 - 在 **查看器与验证器** 部分添加了对 Fiddler 的引用
  • v1.2 - 2017 年 8 月 16 日 - 添加了新的 **递归反序列化** 部分(感谢 BillWoodruff 的建议);更新了 C#/VB WpfMultiSearch 示例的下载。
  • v1.3 - 2017 年 8 月 19 日 - 在 **多值类型集合** 部分添加了新的 **Google Drive** File Explorer 示例应用程序,以演示 **多值类型集合** 的另一种版本,其中包含用于文件类型类支持的多个标识符键。
  • v1.4 - 2017 年 10 月 26 日 - 添加了目录以便于导航;添加了代码生成器 quicktype.io;添加了序列化库 Utf8Json
  • v1.5 - 2022 年 8 月 13 日 - 更新了文章标题;添加了新项目 **WpfUserAgent** + 更新了文章;修复了 MovieDB_MultiSearch.Json 数据文件中的损坏图片链接。
  • v1.51 - 2022 年 9 月 3 日 - 在 **查看器与验证器** 部分添加了 **JsonCrack** & **JsonHero** 工具。
  • v1.52 - 2022 年 11 月 1 日 - 添加了第三篇文章的链接:使用 Newtonsoft.Json & System.Text.Json 与 C# & VB 反序列化 Json 流
© . All rights reserved.