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

JSON 解析器、查看器和序列化器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (10投票s)

2014年7月19日

CPOL

5分钟阅读

viewsIcon

31937

downloadIcon

3126

一个自定义的 JSON 状态机解析器、查看器和 JSON 数据序列化器

引言

我知道 C# 有好几个 JSON 解析器,但我喜欢通过自己动手来理解事物。所以,当我需要探索一些 JSON 格式的服务器输出时,考虑到 JSON 结构非常简单,我决定创建一个自定义解析器来读取它。这是我的工具的核心,用于在树状视图中显示 JSON 数据。

此外,我希望能够将 JSON 数据从 C# 类中(反)序列化。因此,我编写了一个自定义序列化类,它可以处理不同的场景,而无需大量的初始化或属性。

背景

JSON 示例

这只是一个示例,展示了 JSON 数据是什么样的——这是格式化的,当使用服务器输出时,可能会使用最少的空白字符。

{
    "PossibleValues" : {
        "ObjectValue" : { 
            "OnlyValue" : "This is the only value in here" 
        },
        "ArrayValue" : [
            1, 1, 2, 3, 5, 8, 13, 21, "..."
        ],
        "StringValue" : "String",
        "NumberValue" : 3.1415926536,
        "BooleanValue" : true,
        "NullValue" : null
    }, 
    "Lightspeed" : {
        "Value" : 2.99792458e8,
        "Unit" : "m/s"        
    }
} 

表示 JSON 结构

JSON 支持六种数据类型

  • 对象:用花括号 '{ }' 包围,包含名称-值对,其中名称和值由冒号 ':' 分隔,名称-值对由逗号 ',' 分隔。在代码中,这将由一个实现 BjSJsonObjectValue 列表的 BjSJsonObject 类实例来表示,以存储名称-值对。
  • 数组:用方括号 '[ ]' 包围,包含由逗号 ',' 分隔的值。在代码中,这将由一个实现 object 列表的 BjSJsonArray 类实例来表示。
  • 字符串:用引号 '"' 包围。在代码中,这是一个 String
  • 数字:例如 -1.454324e-18 或更简单的形式。在代码中,这表示为 Decimal,以便保持最大的精度。
  • 布尔值:是没有引号的单词 'true' 或 'false'。在代码中,这是一个 Boolean
  • Null:是没有引号的单词 'null'。在代码中,这是 null

JSON 对象和数组都可以包含不同类型的数据,因此名称-值对和数组只包含存储实际值的 C# object。要确定存储的值的类型,您可以使用 is 运算符或枚举的描述值,例如来自辅助函数 BjSJsonHelper.GetValueKind(...)BjSJsonValueKind

JSON 解析器状态机

解析器是一个逐个字符读取输入的有限状态机。当新部分开始时,它会切换状态,非常直接,所以这里就不再深入探讨了——其余的可以在 BjSJsonHelper.BjSJsonReader 的代码中看到。

JSON(反)序列化器

它基本上只有两个公共方法

  • ToJson<T>(T obj):将泛型类型 T 的类序列化为 BjSJsonObject——所有子类型、数组、列表和字典都会自动处理。
  • FromJson<T>(BjSJsonObject obj):将 BjSJsonObject 映射到泛型类型 T 的实例——这里所有子类型、数组、列表和字典也会自动处理。

序列化方法 ToJson<T>(T obj) 遍历给定类型 T 的属性,尝试提取每个值并将其转换为 JSON 表示。

反序列化方法 FromJson<T>(BjSJsonObject obj) 遍历 BjSJsonObject 的属性,尝试在给定类型 T 中查找具有相同名称和合适数据类型的属性。如果找到,JSON 值将被转换为类属性的数据类型。

不过也有一些限制,因为我手动管理数据类型的序列化。所以这个序列化器只支持具有标准构造函数(包括子类)的类,并且只支持以下数据类型的属性

  • null
  • 字符串
  • 数字(byte, sbyte, short, ushort, int, uint, long, ulong, float, double 和 decimal)
  • Guid
  • DateTime(转换为 ISO 格式的字符串)
  • TimeSpan(转换为格式化字符串)
  • Image|Bitmap(转换为 Base64 字符串)
  • 数组(一维数组,所有 T[],其中 T 是这些数据类型之一)
  • List<T>(其中 T 是这些数据类型之一,将被转换为数组——属性的类型定义在反序列化时区分数组和列表)
  • Dictionary<K,V>(其中 K 和 V 各是这些数据类型之一,将被转换为由两个值组成的数组的数组)
  • class 实例(具有标准构造函数,并且只包含这些数据类型的属性)

我尝试跳过其他数据类型——但我没有测试过,因为这些是我需要使用的所有数据类型。

使用代码

用于处理 JSON 对象的主要类是 BjSJsonObjectBjSJsonObjectMemberBjSJsonArray 在 BjSJsonObject 中使用,并构建非简单 C# 类表示的数据类型。BjSJsonConverter 类可用于将 C# 类转换为 JSON 对象并反之。

使用方法非常直接。要加载一个 JSON 对象,只需使用其中一个构造函数即可。

// Load a string
string data = "{\"Member\":\"Value\",\"Another\":3.14}";
BjSJsonObject jObj = new BjSJsonObject(data);

// Load a file directly
string filename = @"C:\data.json";
BjSJsonObject jObj = new BjSJsonObject(filename, Encoding.ASCII);

// Load data from a stream
MemoryStream ms = new MemoryStream(File.ReadAllBytes(filename));
BjSJsonObject jObj = new BjSJsonObject(new StreamReader(ms));

要将其转回 JSON 文本,只需调用 ToJsonString(bool) 方法即可。

BjSJsonObject jObj = new BjSJsonObject(@"C:\data.json", Encoding.ASCII);

// Strip as many white spaces as possible
string jsonData = jObj.ToJsonString(true);

// Format the output to make it easily readable
string jsonData = jObj.ToJsonString(false);  

您可以使用索引器、Add()、Remove() 和 RemoveAt() 方法来访问和修改数据。BjSJsonObject 和 BjSJsonArray 还实现了 IEnumerator 接口并提供了 Count 属性,因此也可以使用 for 和 foreach 循环。下面的示例展示了如何创建一个 BjSJsonObject,填充数据,然后用于添加其他属性,最后将结果保存到文件中。

// Create a new BjSJsonObject
BjSJsonObject jObj = new BjSJsonObject();

// Create a BjSJsonArray, fill it with numbers and add it to the object as property "arr"
BjSJsonArray jArr = new BjSJsonArray();
for (int i = 0; i < 10; i++)
    jArr.Add(Convert.ToDecimal(i));
jObj.Add("arr", jArr);

// Add the square of each array number as a property with the number as name to the object
foreach (object n in jArr)
    jObj.Add(n.ToString(), (decimal)n * (decimal)n);

// Convert the object to text
string jData = jObj.ToJsonString(false);

// Save the data to a file
File.WriteAllText(@"C:\output.json", jData);

BjSJsonConverter 的使用方法非常直接——正如预期的那样。传递的对象会被递归处理。除了对象本身,唯一使用的其他信息是对象的类型。对于转换为 JSON,这仅用于拥有与转换回来时相同的​​方法模式。下面的示例定义并创建了一个 Customer 类的实例,将其转换为 JSON 文本,然后再转换回第二个类的实例。

public class Customer
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Dictionary<string, Address> Addresses { get; set; }
    public List<Customer> SubCustomers { get; set; }
}
public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

public void DoConverting()
{
    // Create test data
    Customer c1 = new Customer()
    {
        Id = Guid.NewGuid(),
        Name = "Max Mustermann",
        Addresses = new Dictionary<string, Address>()
        {
            { "Work", new Address() { Street = "Bahnhofstr. 1", City = "12345 Neustadt" },
            { "Home", new Address() { Street = "Postweg 34", City = "12345 Neustadt" }
        },
        SubCustomers = new List<Customer>()
        {
            new Customer()
            {
                Id = Guid.NewGuid(),
                Name = "Tanja Müller",
                Addresses = null,
                SubCustomers = null
            },
            new Customer()
            {
                Id = Guid.NewGuid(),
                Name = "Steffan Taylor",
                Addresses = null,
                SubCustomers = null
            }
        }
    }

    // Convert the object to Json text
    string jText = BjSJsonConverter.ToJson(c1).ToJsonString(true);

    // Convert the Json text back into another instance of Customer
    BjSJsonObject jObj = new BjSJsonObject(jText);
    Customer c2 = BjSJsonConverter.FromJson<Customer>(jObj);
} 

JsonViewer 项目只是一个测试场景和我使用的工具。

关注点

BjSJsonConverter 中的转换代码非常动态。它可以轻松用于其他目的,例如 ORM。我绝对建议任何想开始使用 .NET 反射的人都研究一下它。

© . All rights reserved.