小型 LINQ to JSON 库






4.96/5 (28投票s)
一个小的 C# LinqToJSON 库,以及它是如何工作的
引言
在这里,您会找到一个用 C# 编写的(15KB)小型 .NET 程序集,它允许读取和写入格式化为 JSON 标准的数据,其功能模仿了 LinqToXML 的功能。
背景
我对 .NET Framework 4.0 中 LinqToXml 读取和写入 XML 数据是多么容易给我留下了深刻的印象,我希望 JSON 也能有类似的东西。我搜索了一下,但找不到我喜欢的东西。
我找到的大多是 JSON 序列化/反序列化程序集,这并不是我感兴趣的,而且我找到的少数 LinqToJSON 辅助工具在我看来存在一些问题
- 它们依赖于底层序列化器
- 它们似乎不遵守 JSON 规范(这主要是由于使用了序列化器)
- 它们不像 LinqToXml 那样提供写入 JSON 数据的方式
当我写“似乎不遵守 JSON 规范”时,我的意思是序列化器写入的数据不符合 Json,正如 www.json.org 所描述的那样。例如,在 JSON 库的示例中,我经常看到类似这样的内容:
"Expiry" = new Date(1234567890)
根据我对 JSON 的理解,这是错误的,因为值 new Date(1234567890)
既不是
- JSON 字符串 - 它应该是:
"
new Date(1234567890)
" - JSON 数字、JSON 布尔值或 JSON
null
– 这个很清楚 - JSON 数组元素 – 它需要分隔符
[
和]
,并且它需要是一个有效的数组元素,而它不是 - JSON 对象 - 它需要分隔符
{
和}
,并且它需要是一个有效的对象成员,而它不是
日期可以用正确的 JSON 格式写入。要么作为字符串:“01/01/2011”,要么作为数组 [1, 1, 2011],要么只是一个数字(通常是自...以来的秒数),并选择一个描述性的名称。例如
"ExpiryDate" : "01/01/2011"
或
"TestDates" : [[01,01,2011], [01,01,2012], [01,01,2013]]
当然,您之后必须将 string
或数组或其他内容转换为实际的 'Date
' 对象,但这应该不是问题,特别是 .NET Framework 已经提供了实现这一目标的必要机制。
再给一个建议:当您决定了非平凡项(无论是 'Date
' 还是二进制数据,或其他什么)的表示方式后,**请坚持下去!**
总之,那就只剩一件事可做了:写我自己的库。
免责声明
我并不声称这个 LinqToJson 库是最快、最精简、最小或最好的,我只声称它准确地完成了我计划要做的事情,并且做得足够好。而且“仅”用了 14KB。:)
此外,请注意,我并不是在批评使用 JSON 序列化器。我的观点是我不需要/不需要序列化器。换句话说,JSON 序列化器并不比我的 LinqToJSON 库更好或更差,它们只是服务于不同的目的,而这些目的不符合我的需求。所以,如果您需要一个序列化器,请使用一个。
规格(我想要的)
公共对象
与 LinqToXml 中的 XDocument
、XElement
和 XAttribute
类似,我想要
JDocument
,作为根对象。JObject
的派生类,以允许直接使用JObject
的属性和方法。- 通用容器
JObject
和JArray
包含值。它们实现IJValue
接口并提供 JSON 内容的访问器。
- 特定容器
JString
– 我不想直接使用内置类型System.String
,因为概念上 JSON 字符串和 C# 字符串是两个不同的项。JString
实现IJValue
接口并根据 JSON 规范检查给定 C#string
的有效性。JNumber
– 这里的情况与为什么我不直接使用Double
类型相同。此类实现IJValue
接口并根据 JSON 数字规范检查给定 C#string
的有效性。JTrue
、JFalse
和JNull
- 它们也实现IJValue
接口。同样,我不想直接使用bool
和object
类型。
读取 JSON
- 提供一个
static
方法JDocument.Load()
- 访问器将返回
- 当调用对象是
JObject
或Jarray
时返回IEnumerable<IJvalue>
,以允许进行 Linq 式查询 - 适合容器的类型和内容值:
JTrue
和JFalse
为 'bool
',JNumber
为 'double
',JString
为 'string
',JNull
为 'object
'。
- 当调用对象是
- 通用类型是
IJValue
。可以使用 is: 关键字通过类型来过滤返回值:
if(value is JNumber).
有关更多详细信息,请参阅下面的类描述以及演示应用程序的代码。
来自演示应用程序的代码片段
// first, load a file
JDocument jDoc = JDocument.Load(filename);
// then, you can use LINQ to parse the document
// the line below does nothing useful, but it works, and that's why it's here
var members = from member in jDoc.GetMembers() select member;
foreach (var member in members)
{
// you can filter the values using their type
if (member.Value is JString || member.Value is JNumber)
// do something ...
// you can filter the values using their type
if (member.Value is JObject)
{
JObject obj = member.Value as JObject;
var names = from name in obj.GetNames() select name;
foreach (var name in names)
{
// you can get an object with its name
IJValue value = obj[name];
if (value is JFalse || value is JTrue || value is JNull)
// do something ...
}
var members = from member in obj.GetMembers() select member;
foreach (var member in members)
{
Console.WriteLine("\t{0} : {1}", member.Name, member.Value.ToString());
}
}
}
写入 JSON
- 创建一个新的
JDocument
实例 - 通过多种方式添加内容。
- 要么直接在 JSON 值 的构造函数中
- 要么使用
Add()
系列方法
- 每个实现
IJValue
接口的类都提供一个返回string
的方法,该string
包含正确的表示形式。对于大多数对象,我不会使用ToString()
,因为我希望 JSONstring
表示形式与 C# 分开。 - 提供一个
JDocument
实例方法Save()
有关更多详细信息,请参阅下面的类描述以及演示应用程序的代码。
代码片段 #1(来自演示应用程序)
// example of JDocument written only through the constructor:
new JDocument(
new JObjectMember("D'oh",
new JObject(
new JObjectMember("First Name", new JString("Homer")),
new JObjectMember("Family Name", new JString("Simpson")),
new JObjectMember("Is yellow?", new JTrue()),
new JObjectMember("Children",
new JArray(
new JString("Bart"),
new JString("Lisa"),
new JString("Maggie"))
)
)
),
new JObjectMember("never gets older",
new JObject(
new JObjectMember("First Name", new JString("Bart")),
new JObjectMember("Family Name", new JString("Simpson"))
)
)
).Save("simpsons.json");
上面的示例中有几个有趣方面
- 除了最后的
Save()
调用外,所有数据都通过构造函数创建 - 代码可以以非常直观的方式呈现,模仿 JSON 数据的结构
- 这个 JSON 数据很可能无法反序列化,主要是因为对象成员名称中包含非字母数字字符
代码片段 #2(来自演示应用程序)
// you can create an empty object and add values
JObject obj = new JObject();
obj.Add("_number", new JNumber(-3.14));
obj.Add("_true", new JTrue()); // notice the use of JTrue
obj.Add("_null", new JNull()); // notice the use of JNull
// you can create an empty array and add values
JArray arr = new JArray();
// ... either only one value
arr.Add(new JNumber("-15.64"));
// ... or more than one at once
// Notice that prefixing your strings with @ will help keeping them as valid JSON strings
arr.Add(new JString(@"Unicode: \u12A0"),
new JString(@"\n\\test\""me:{}"));
JDocument doc = new JDocument(
new JObjectMember("_false", new JFalse()),
new JObjectMember("_false2", new JFalse())
); // the same name cannot be used twice!
// Add() has two forms:
// 1. with JObjectMember, you can give one or more
doc.Add(
new JObjectMember("_array", arr),
new JObjectMember("_string1", new JString("string1")),
);
// 2. directly give the name and the value
doc.Add("_obj", obj);
doc.Save(filename);
接口和类
接口
IJValue
– 代表 JSON 值可能包含的所有不同项的通用类型。
此接口声明两个方法:ToString()
和 ToString(int indentLevel)
。
公共类
此处仅列出重要的 public
方法和属性(public
关键字故意省略)。
class JDocument : JObject
{
JDocument()
// Creates a new JDocument instance and copies the members from jObject
JDocument(JObject jObject)
// Creates a new JDocument instance and fills it with the jObjectMembers
JDocument(params JObjectMember[] jObjectMembers)
// Loads JSON data from a file. An exception is thrown if something goes wrong.
// The file has to contain nothing but JSON data.
// * "uri" : URI (path) to the file. Must contain only JSON Data
// * "encoding" : specific encoding (default is UFT8Encoding)
public static JDocument Load(string uri)
public static JDocument Load(string uri, System.Text.Encoding encoding)
// Loads JSON data from a stream. An exception is thrown if something goes wrong.
// The stream has to contain nothing but JSON data from its current position to its end.
// * "stream": stream from where to read.
// The stream object is allowed to not support stream.Length
// * "encoding" : specific encoding (default is UFT8Encoding).
public static JDocument Load(Stream stream)
public static JDocument Load(Stream stream, System.Text.Encoding encoding)
如果您为编码提供 null
作为参数,那么 Load()
将尝试根据 ByteOrderMark
检测编码类型(当前检测到:UTF16 (BE & LE)、UTF8 和 ASCII)。如果失败,则抛出异常。
// Write the JDocument as JObject to the stream. The stream is not closed.
// An exception is thrown if something goes wrong.
// * "stream" : stream where to write
// * "encoding" : specific encoding (default is UFT8Encoding)
// * "addByteOrderMark" : whether or not the Byte Order Mark has to be added to the file
// The BOM may be empty depending on the properties of "encoding".
public void Save(Stream stream)
public void Save(Stream stream, System.Text.Encoding encoding, bool addByteOrderMark)
// Saves the JSON data to a file.
// * "uri" : URI (path) of the file. If it already exists, it will be overwritten
// * "encoding" : specific encoding (default is UFT8Encoding)
// * "addByteOrderMark" : whether or not the Byte Order Mark has to be added to the file
public void Save(string uri)
public void Save(string uri, System.Text.Encoding encoding, bool addByteOrderMark
// Parses the given text. Only JSON data is expected.
// An exception is thrown if something goes wrong, for instance if the JSON data
// is not properly formatted
static JDocument Parse(string text)
}
class JObject : IJValue
{
// Creates an empty JSON object. Use one of the Add() functions to fill it.
JObject()
// Creates a JSON object pre-filled with the jObjectMembers.
// Further members can still be added using one of the Add() functions
JObject(params JobjectMember[] jObjectMembers)
// Returns the amount of members in the object
int Count
// Returns the object member value that is associated to 'name'
IJValue this[string name]
// Returns the object member value that is associated to 'name'
IJValue GetValue(string name)
// Returns a list of all the names stored in the object, without their values
Ienumerable<string> GetNames()
// Returns a list of all the values stored in the object, without their names
IEnumerable<IJValue> GetValues()
// Returns a list of all members stored in the object
IEnumerable<JObjectMember> GetMembers()
// Adds JSON object members (the values and their associated name are
// stored in a JObjectMember object) to the JSON object
void Add(params JobjectMember[] jObjectMembers)
// Adds one JSON object member to the object.
// A name cannot be added twice, it has to be unique in the object
void Add(string name, IJValue jValue)
}
// data container class, used solely with JObject
class JObjectMember
{
string Name // only the getter is public
IJValue Value // only the getter is public
JObjectMember(string name, IJValue value)
}
class JArray : IJValue
{
// Creates an empty JSON array. Use one of the Add() functions to fill it.
JArray()
// Allows to set the initial capacity of the private value container.
// Use one of the Add() functions to fill it.
JArray(int capacity)
// Creates a JSON array pre-filled with the jValues.
// Use one of the Add() functions to fill it further.
JArray(params IJValue[] jValues)
// Returns the amount of elements in the array
int Count
// Returns the value stored at a specific index in the JArray
IJValue this[int index]
// Adds JSON values to the JSON array
void Add(params IJValue[] jValues)
// Returns a list of all elements stored in the JSON array
IEnumerable<IJValue> GetValues()
}
class JNumber : IJValue
{
double Content
JNumber(double number)
// 'text' is checked for validity according to the format description on
// <a href="http://www.json.org/">www.json.org</a>, an exception is thrown if an
// issue is found.
JNumber(string text)
}
class JString:IJValue
{
string Content
// 'content' is checked for validity according to the format description on
// <a href="http://www.json.org/">www.json.org</a>, an exception is thrown if an
// issue is found.
JString(string content)
}
class JNull : IJValue
{
object Content // returns 'null'
}
class JFalse : IJValue
{
bool Content // returns 'false'
}
class JTrue : IJValue
{
bool Content // returns 'true'
}
错误处理
错误仅通过异常通知,不返回任何错误代码。
演示控制台应用程序
演示应用程序是一些零散的代码,用于展示使用 Ranslant.Linq.Json
程序集读取和写入 JSON 数据的几种方法。
它还显示了在解析或保存时可能触发错误的情况。
关注点
源代码
我试图编写易于理解但又足够高效的代码。我避免了复杂的结构和设计模式,因为读取/写入 JSON 数据本身并不是一项复杂的任务,因此**不应该需要复杂的代码**。
有几点对 C# 初学者来说值得一提
- 正则表达式 的使用(例如在
JNumber
中) JObject
和JArray
中的数据结构的延迟创建(Add()
)。只有在实际需要时,才会初始化private
数据容器。- 扩展方法(
ConsumeToken()
)的使用 - 接口作为通用基类型(
IJValue
)的使用,结合简单的工厂模式(JParser.ParseValue()
) - 具有可变数量参数的方法(例如
Add
(params JObjectMember
[] jObjectMembers
)
) - 使用
IEnumerable<T>
来利用Linq 的查询能力 - 使用
new
语句来隐藏继承的方法(ToString()
)
关于解析器
编写文本解析器有成百上千种方法。我认为只要文本被正确解析并获得指定的(并且希望是预期的)结果,它们都是正确的。这意味着:我的解析器是好的,因为它有效。这就是我对它的全部期望。
我在这里做的事情很简单,而且相当标准。我解析文本以查找标记。这些标记代表特定 JSON 值类型的分隔符。换句话说,一个标记或一组标记揭示了预期的值类型。然后检查这些标记之间的文本是否有效,这意味着我检查我拥有的内容是否实际上代表了我从分隔符处预期的值类型。
例如,分隔符 '{' 意味着我得到一个 JSON 对象,因此这个分隔符和相应的结束分隔符 ('}') 之间的所有内容都应该代表一个 JSON 对象,因此应该按照语法规则(取自 www.json.org)编写
- JSON
object
是{}
或{members}
members
是pair
或pair,members
pair
是string
: value
string
是""
或"text"
,其中text
是……value
是object
或array
或string
或number
,等等。- 等等。
解析器所做的是遵循这些语法规则,并在规则被违反时(通过抛出异常)报告错误。
string
在技术上是如何解析的,是通过“消耗”它,直到它变为空。换句话说,检查(无论是标记还是内容)的内容会从要解析的 string
中移除。想象一下 Pac-Man 在迷宫中一次吃一个药丸。这里也是一样。:)
为了实现这一点,我选择了以下代码结构
- 一个包含将要被消耗的
string
的类字段。此字段不是static
的,因为我想保持线程安全。每个解析步骤都会修改此string
,通过删除当前标记或内容。 - 一组解析函数,每个函数专门用于识别和/或填充一种且仅一种类型的值。调用哪一个取决于找到的标记。
我还考虑了以下替代方案,其中类中不存储 string
,而是从一个解析函数传递到另一个解析函数
ParseSomething(string textIn, out string textOut)
其中 textIn
是传递给特定 parse
函数的 string
,而 textOut
是成功解析后剩余的 string
。但我放弃了这个解决方案,因为担心性能。事实上,JSON 的递归性质会创建许多必需的 string
实例的情况,需要大量内存。
将基类转换为其派生类之一
解析器的第一个具体困难是让 ParseDocument(string)
返回一个 JDocument
实例,尽管它实际上是在解析一个 JObject
。
最初我使用了以下代码
Document jDoc = (JDocument)ParseObject();
它编译了,但抛出了 InvalidCastException
(请参阅 http://msdn.microsoft.com/en-us/library/ms173105.aspx 并搜索“Giraffe”)。实际上 C# 中没有与 C++ dynamic_cast
等效的功能,因此您不能将基类转换为其派生类之一。
这个问题有几种解决方案
- 选定的解决方案 - 将
ParseObject()
返回的对象成员复制到JDocument
对象的基类中(使用构造函数JDocument(JObject jObject)
)。这效果很好,只需要很少的代码。此外,此解决方案允许通用地从任何JObject
创建JDocument
,这也可能很有用。 - 将
JObject
实例封装在JDocument
中。我发现这在可用性方面不如第一个解决方案。 - 使用
JObject
的CopyTo()
方法。这使用了反射(因此可能带来性能问题),并且根据 http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/f7362ba9-48cd-49eb-9c65-d91355a3daee,不保证能正常工作 - 让
JDocument
派生自IJValue
。但那样我就失去了继承JObject
方法,并且我需要在JDocument
中复制它们。这绝对不干净。 - 我检查了是否可以使用
in
和out
关键字,看看是否可以使用 C# 的协变和抗变特性,但由于我没有使用泛型,所以这行不通。
我发现解决方案 #1 是唯一干净优雅的解决方案,但它之所以有效,是因为在 JObject
中,字段和属性(实际上只有一个字段)仅通过 public
访问器函数(Add()
函数)进行读写,这些函数由于继承关系可以在 JDocument
中自由使用。
这意味着这种模式不能通用使用。
阅读源代码和其中的注释以获取更多信息。
Unicode 编码
第二个困难是处理不同的文本编码。ASCII 不是问题,但 UTF8/16/32 是:您需要提供正确的编码器才能获得正确的 C# string
。
此外,在内部,C# string
是使用 UTF16 编码的,根据数据的原始编码,在转换为 C# string
时,您会或不会得到一个字节顺序标记(请参阅维基百科关于 UTF 8 和 UTF 16 的文章以获取更多信息)。
为了解决这个问题,我在 JDocument
中添加了 Load()
/ Save()
函数的两个变体
- 可以指定编码的多态原型(
System.Text.Encoding
) - 当未指定编码时,UTF8 是默认值
- 如果在
Load()
函数的编码参数中传递null
,则库将尝试根据字节顺序标记检测编码,如果失败,则抛出异常。
此外,由于 UTF 32 很少使用,所以我根本没有考虑它。
我建议仔细阅读关于 System.Text.Encoding
的 MSDN 文档。
可能的改进
源代码的清晰度和效率
显然,您会把所有东西写得完全不同,而且更好。 :D
更严肃地说,如果您对代码清晰度(我指的是清晰代码开发)以及数据和类结构的效率有建议,那么欢迎您,我一直想学习。
Bug 修复
我没有在包中包含单元测试,但我已经彻底测试了代码。不过,可能仍有一些问题被我忽略了,所以如果您发现错误,欢迎报告(请足够详细)。
如果您有一个此代码无法读取的 JSON 文件
- 请确保 JSON 文本格式正确
- 如果可能,请将文件发给我,我会检查
直接转换
对于 JTrue
和 JFalse
,使用 bool
,对于 JNumber
,使用 double
,对于 JString
,使用 string
,对于 JNull
,使用 object
,可能会更直接。我仍然会保留这些类,因为我仍然想使用 IJValue
接口,但我可以添加转换器,以便能够这样写
var myQuery = from value in jDocument.GetValues() select value;
foreach(IJValue value in myQuery)
{
if(value is JTrue || value is JFalse)
{
if(value)
{
// do something...
}
}
}
目前您必须写:if(value.Content)
,这不算太多要求,但 if(value)
更方便,而 LINQ 就意味着方便,不是吗?
这会有帮助吗?
JSON 到 XML 转换器
对此不太确定。用户或许可以提供一个 schema 来提供所需的结构,因为我绝对不想强加一个 XML schema 给用户。但我不太确定这是否需要。
解析器
- 使用更多正则表达式?例如,检查
string
内容的有效性 - 提高解析速度?如何?为什么?
- 支持注释(分隔符:@"//"),即使这在 JSON 标准中没有定义?
Generic
- 改善内存占用?
- 让实现
IJValue
的类也实现IDisposable
?为什么?
历史
- 2011/11/23
- 初始文章
- 2011/11/24
- 我在代码中发现了一些可以重构的地方,我现在使用
public new ToString()
将 JSON 数据写为文本。 - 2011/11/29
JDocument
- 更改了Save(...)
和Load(...)
方法以使用Stream
对象,并可以选择指定文本编码。- 2011/11/30
- 我发现了编码器和 UTF8/16 编码数据的
ByteMarkOrders
的一个问题。所以我添加了以下更改: - 如果您为编码提供“
null
”作为参数,那么Load()
将尝试根据ByteOrderMark
检测编码类型(当前检测到:UTF16 (BE & LE)、UTF8 和 ASCII) - 通过保存,您可以指定是否需要 BOM
- 解析时,如果检测到 BOM,则会忽略它
- 2011/12/06
- 希望这是最后一次更新
- 在文章中添加了缺失的文本(Unicode 编码)
- 修复了一些拼写错误
- 上传了库及其源代码的最新(最终?)版本(在检测不以可见字符开头的数据的 ASCII 编码时出现了一个问题)