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






4.71/5 (10投票s)
一个自定义的 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 对象的主要类是 BjSJsonObject
。BjSJsonObjectMember
和 BjSJsonArray
在 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 反射的人都研究一下它。