2024-08-02 -> by:DebugST

STLib.Json

简介

STLib.Json是一款基于MIT开源协议的Json解析库。由DebugST开发,该库纯原生实现不依赖任何库,所以非常轻量便捷,且功能强大。由于没用任何依赖纯原生构建,它可以很容易移植到其他编程语言中。当然由于作者懒的一批,这个概率不是很大,做做梦就好了不要太当真。

那么为什么要开发STJson呢?众所周知,.NET自带的解析库用户体验过于友好,想必大家肯定不太习惯使用。而且刚好作者目前也总是需要使用到JSON数据的处理。虽然有第三方的一些库可以使用,但是在作者体验后还是觉得,算了自己动手吧。主要是两点

在设计STJson的时候作者没有任何的异议,毕竟Json的数据格式不存在有争议的地方,当然在兼容JSON5的时候作者还是加入部分自己的想法,反正目前的JSON5也并不是正式的文档。

当在设计STJsonPath的时候,作者犹豫了,因为作者发现很多存在争议的地方,且无法找到关于JsonPath的正式RFC文档,仅仅找到一份草案:

JSONPath: Query expressions for JSON

虽然草案中一些疑惑也能得到解决,但是作者仍有一些疑惑,毕竟不是正式的文档。在开发之前作者也好奇其他JsonPath的开源库,对于有争议的地方其他库是如何解决的。但很遗憾,问题依然存在。似乎没有库愿意搭理这份草案。既然JsonPath还没有正式的RFC文档,只能说作者会尽可能兼容草案的同时,还会加入一些作者自己的想法。

路劲疑问

JsonPath是源自于XPath的使用方式,总所周知XPathXML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言。显而易见JsonPath是用于Json的。它们两种几乎使用方式相同,但是XMLJson两种数据格式的差异,必然会有存在不同的地方。

比如在XML的元素名中是不存在特殊字符的,可是JsonKey可以是任意的字符串,假如有一下Json数据:

{ "aa": { "bb": "cc" } }

JsonPath中我们可以通过路径aa.bb获取到值cc,这没有任何问题,如果Json数据换成下面的呢:

{ "a.a": { "bb": "cc" } }

很显然通过a.a.bb是无法得到值cc的。或许通常情况下Json都表示某个对象,而对象是不会存在这么奇怪的属性的。但是对于Json的数据格式而言,它的Key可以是任意字符。即便在浏览器中通过{OBJ_NAME}.a.a.bb也是只会得到报错。但是浏览器访问Json元素的方式并不是只用通过.的方式去获取,还有索引器{OBJ_NAME}['a.a']['bb']是可以得到正确值的。

STJsonPath中允许使用'或者",在词法分析器(STJsonPathTokenizer.cs)中会将其标记为String,若此字符串在非表达式的作用域内,解析器(STJsonPathParser.cs)会将其重新标记为Property作为索引使用,从而避免特殊字符串无法处理的情况,字符串支持\进行转义。所以在STJsonPath中可以使用'a.a'.bb获取到正确值。而在草案中也确实提到过类似的处理方式。

表达式疑问

在草案中有提到支持表达式,且表达式有两种类型,()?()。分别代表普通表达式过滤表达式。而按照作者的理解,普通表达式用于计算出一个值,且将这个值作为JsonPath的一部分,比如在很多案例中可以看到的$.books[(@.length - 1)]。在执行JsonPath的时候会可能对Json元素进行层层递归,其中@是一个动态变量,表示递归过程中当前正在处理的Json元素,根据表达式的表面意思是想获取books中的倒数第一个元素。

但是@.length如何被执行?length是哪里来的?如果@是一个数组,作者暂且可以理解为是在对数组求长度?可是如果@是一个对象呢?

既然是表达式,那么可以在()中写什么样的语法呢?语法规呢?关于这些疑问作者都采用了自己的实现方式,将在稍后的教程中介绍。

STJson API

数据类型关系

.NetSTJsonValueType.NetSTJsonValueType
byteLongsbyteLong
shortLongushortLong
intLonguintLong
longLongulongLong
floatDoubledoubleDouble
decimalDoubleboolBoolean
charStringstringString
DateTimeStringenumLong or String
PointArrayPointFArray
SizeArraySizeFArray
RectangleArrayRectangleFArray
ColorArrayDataTableObject
ArrayArrayICollectionArray
IDectionaryObjectobjectObject

STJson涵盖了常见的基本数据类型,即便不包含在其中,那么最后也会通过执行反射递归对象的属性。

静态函数

下列中(+n)表示有多个重载

returnsignaturenote
stringSerialize(+n)object对象序列化为字符串。
STJsonDeserialize(string)将字符串转换为STJson对象序。
TDeserialize<T>(+n)将对象或字符串转换为目标对象。
STJsonWriterWrite创建一个STJsonWriter对象。
STJsonReaderRead创建一个STJsonReader对象。
STJsonCreate(STJsonCreator)创建一个Json对象。
STJsonSTJson CreateObject()创建一个空白对象。
STJsonSTJson CreateArray(params object[])创建一个数组对象。
STJsonFromObject(+n)将一个对象转换为STJson
stringFormat(+n)格式化一个Json字符串。
voidAddCustomConverter(+n)自定义类型转换器。
voidRemoveCustomConverter(+n)移除自定义转换器。

非静态函数

returnsignaturenote
STJsonSetItem(+n)向对象中添加一个键值对,并返回自己。
STJsonSetKey(string)向对象中添加一个key,并返回目标对象。
voidGetValue(+n)获取目标对象的值。
voidSetValue(+n)设置目标对象的值。
STJsonDelete(string)从对象中移除一个key,并返回目标对象。
STJsonAppend(+n)向数组对象中添加一个或一些元素,并返回自己。
STJsonInsert(+n)向数组对象中插入一个元素,并返回自己。
STJsonRemoveAt(int nIndex)从数组对象中删除一个索引。
voidClear()清空所有子元素。
IEnumerator<STJson>GetEnumerator()获取当前元素中所有子元素。
STJsonClone克隆当前元素。

扩展函数

returnsignaturenote
boolIsNullOrNullValue判断当前对象是否为空或者空值。
STJsonSet(+n)根据路径(STJsonPath)设置对象。
stringGetValue(+n)获取对象的字符串值(仅值类型)
TGetValue<T>(+n)获取对象值(仅值类型)
STJsonSelect(+n)在对象中进行数据筛选。
STJsonSelectFirst(+n)在对象中进行数据筛选,并选中第一个结果。
STJsonSelectLast(+n)在对象中进行数据筛选,并选中最后一个结果。
STJsonGroup(+n)对指定字段进行分组。
STJsonTerms(+n)对指定字段的值进行个数统计。
STJsonSort(+n)对指定字段进行排序处理。
STJsonMin(+n)统计指定字段的最小值。
STJsonMax(+n)统计指定字段的最大值。
STJsonSum(+n)统计指定字段的总数。
STJsonAvg(+n)统计指定字段的平均值。

字段

typenamenote
stringKey当前STJson父元素的Key
objectValue当前STJson的值,若当前STJson不是值类型则为null
boolIsNullValue当前STJson是否为空元素,即无法确定当前STJson值的数据类型。
intCount当前STJson所包含子元素的个数。
STJsonValueTypeValueType(枚举)当前元素的数据类型。

STJsonValueType为以下值:

Undefined String Boolean Long Double Datetime Array Object

索引器

其他对象

名称note
STJsonCreator用于创建一个复杂STJson对象
STJsonReader用于从一个TextReader中动态解析对象。通常用于从文件中读取一个较大数据的Json
STJsonWriter用于向一个TextWriter中写入Json字符串。通常用于直接向文件中写入一个较大切复杂的Json数据。

STJson [基本应用]

STJson是一个中间数据类型,它是stringobject之间的桥梁,使用非常便捷,比如:

var st_json = new STJson()
    .SetItem("number", 0)               // 函数返回自身 所以可以连续操作
    .SetItem("boolean", true)
    .SetItem("string", "this is string")
    .SetItem("datetime", DateTime.Now)
    .SetItem("array_1", STJson.CreateArray(123, true, "string"))
    .SetItem("array_2", STJson.FromObject(new object[] { 123, true, "string" }))
    .SetItem("object", new { key = "this is a object" })
    .SetItem("null", obj: null);
st_json.SetKey("key").SetValue("this is a test");
Console.WriteLine(st_json.ToString(4)); // 4 -> indentation space count
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "number": 0,
    "boolean": true,
    "string": "this is string",
    "datetime": "2023-04-22T21:12:30.6109410+08:00",
    "array_1": [
        123, true, "string"
    ],
    "array_2": [
        123, true, "string"
    ],
    "object": {
        "key": "this is a object"
    },
    "null": null,
    "key": "this is a test"
}

当执行var st_json = new STJson()时,st_json为空元素,即st_json.IsNullValue = true。因为此时无法确定st_json对象还是数组或者是

STJson中不存在类似于JArrayJObject对象,STJson既可以是Array也可以是ObjectSTJson拥有两个索引器[int][string]

var json_1 = new STJson();
Console.WriteLine("[json_1] - " + json_1.IsNullValue + " - " + json_1.ValueType);

var json_2 = new STJson();
json_2.SetItem("key", "value");
Console.WriteLine("[json_2] - " + json_2.IsNullValue + " - " + json_2.ValueType);

var json_3 = new STJson();
json_3.Append(1, 2, 3);
Console.WriteLine("[json_3] - " + json_3.IsNullValue + " - " + json_3.ValueType);

var json_4 = new STJson();
json_4.SetValue(DateTime.Now);
Console.WriteLine("[json_4] - " + json_4.IsNullValue + " - " + json_4.ValueType);

var json_5 = STJson.CreateArray();          // made by static function
Console.WriteLine("[json_5] - " + json_5.IsNullValue + " - " + json_5.ValueType);

var json_6 = STJson.CreateObject();         // made by static function
Console.WriteLine("[json_6] - " + json_6.IsNullValue + " - " + json_6.ValueType);

var json_7 = STJson.FromObject(12);         // made by static function
Console.WriteLine("[json_3] - " + json_7.IsNullValue + " - " + json_7.ValueType);
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[json_1] - True - Undefined
[json_2] - False - Object
[json_3] - False - Array
[json_4] - False - Datetime
[json_5] - False - Array
[json_6] - False - Object
[json_7] - False - Long

通常情况下SetItem(+n)会将ValueType设置为Object,而Append(+n)Insert(+n)会将ValueType设置为Array

正如上面提到的STJson有两个索引器,可以通过索引器访问他们,或者获取值。

var json_temp = STJson.CreateArray()
    .SetItem("string", "this is string")
    .SetItem("array", new Object[] { "1", "2", "3" });
Console.WriteLine(json_temp["string"]);
Console.WriteLine(json_temp["string"].GetValue());
Console.WriteLine(json_temp["array"][1]);
Console.WriteLine(json_temp["array"][1].GetValue<long>());
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
"this is string"
this is string
"2"
2

object -> string

通过上面的例子或许你已经知道怎么将一个对象转换为string,通过STJson.FromObject(object).ToString(+n)即可,但是有没有可能,其实不用这么麻烦的?比如:STJson.Serialize(+n)就可以了???

事实上STJson.Serialize(+n)的效率会更好,因为它是直接将对象转换为字符串,而不是转换成STJson再转换成字符串。

Console.WriteLine(STJson.Serialize(new { key = "this is test" }));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{"key":"this is test"}

当然你可以有个更友好的输出格式:

Console.WriteLine(STJson.Serialize(new { key = "this is test" }, 4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "key": "this is test"
}

或者你可以直接序列化到一个TextWriter中去:

STJson.Serialize(new { key = "this is test" }, new StreamWriter("./test.json")));

string -> object

事实上代码并不会直接将string转换为object。因为在那之前必须先对字符串进行解析,确保它是一个正确格式的Json。但是做完这个过程的时候已经得到一个STJson对象了。最后将STJson再转换为object

所以你会在源代码STLib.Json.Converter中看到如下文件:

ObjectToSTJson.cs ObjectToString.cs STJsonToObject.cs

里面并没有StringToObject.cs文件,而STJson.Deserialize(+n)的源码如下:

public static T Deserialize<T>(string strJson, +n) {
    var json = STJsonParser.Parse(strJson);
    return STJsonToObject.Get<T>(json, +n);
}

STJson -> object

如何将字符串转换为对象,相信作者不用说明读者也应该知道如何处理,但是这里值得说明的是,STJson可以附加到对象中,实现局部更新。

public class TestClass {
    public int X;
    public int Y;
}

TestClass tc = new TestClass() {
    X = 10,
    Y = 20
};
STJson json_test = new STJson().SetItem("Y", 100);
STJson.Deserialize(json_test, tc);
Console.WriteLine(STJson.Serialize(tc));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
 {"X":10,"Y":100}

STJsonConverter

虽然在STJson中内置了很多数据类型的转换,即便没有的数据类型也会被当做object做递归处理。但是有时情况并不是很友好。比如:

Rectangle rect = new Rectangle(10, 10, 100, 100);
Console.WriteLine(STJson.Serialize(rect, 4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "Location": {
        "IsEmpty": false,
        "X": 10,
        "Y": 10
    },
    "Size": {
        "IsEmpty": false,
        "Width": 100,
        "Height": 100
    },
    "X": 10,
    "Y": 10,
    "Width": 100,
    "Height": 100,
    "Left": 10,
    "Top": 10,
    "Right": 110,
    "Bottom": 110,
    "IsEmpty": false
}

很显然,这个结果过于复杂,因为Rectangle的所有字段都被递归出来了。但是,如果这样呢?

public class RectangleConverter : STJsonConverter
{
    public override object JsonToObject(Type t, STJson json, ref bool bProcessed) {
        return new Rectangle(
            json["x"].GetValue<int>(),
            json["y"].GetValue<int>(),
            json["w"].GetValue<int>(),
            json["h"].GetValue<int>());
    }

    public override STJson ObjectToJson(Type t, object obj, ref bool bProcessed) {
        Rectangle rect = (Rectangle)obj;
        return STJson.New()
            .SetItem("x", rect.X)
            .SetItem("y", rect.Y)
            .SetItem("w", rect.Width)
            .SetItem("h", rect.Height);
    }

    public override string ObjectToString(Type t, object obj, ref bool bProcessed) {
        //return "{\"x\":" + ... + "}"
        var json = this.ObjectToJson(t, obj, ref bProcessed);
        if (bProcessed) {
            return json.ToString();
        }
        return null;
    }
}

Rectangle rect = new Rectangle(10, 10, 100, 100);
STJson.AddCustomConverter(typeof(Rectangle), new RectangleConverter());
string strResult = STJson.Serialize(rect);
Console.WriteLine(strResult);
rect = STJson.Deserialize<Rectangle>(strResult.Replace("100", "200"));
Console.WriteLine(rect);
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{"x": 10,"y": 10,"w": 100,"h": 100}
{X=10,Y=10,Width=200,Height=200}

其中bProcessed默认传入值为true,当上一层函数获取到false时则使用默认的处理方式。

STJsonConverter提供了Attribute类,也可用于标记对象属性。

public class Test{
    [STJsonConverter(typeof(RectangleConverter))]
    public Rectangle Rect{get; set;}
}
STJsonConverter.cs
public abstract class STJsonConverter
{
    public virtual STJson ObjectToJson(Type t, object obj, ref bool bProcessed) {
        bProcessed = false;
        return null;
    }
    public virtual string ObjectToString(Type t, object obj, ref bool bProcessed) {
        bProcessed = false;
        return null;
    }
    public virtual object JsonToObject(Type t, STJson json, ref bool bProcessed) {
        bProcessed = false;
        return null;
    }
}

STJsonAttribute

或许在序列化的时候你并不想输出所有的属性,那么可以通过STJsonAttribute去控制。

[STJson(STJsonSerilizaMode.Include)]    // optional
public class Student
{
    [STJsonProperty("test_name")]
    public string Name;
    public int Age;
    public Gender Gender;
    [STJsonProperty]                        // optional
    public List<string> Hobby;
}

public enum Gender
{
    Male, Female
}

var stu = new Student() {
    Name = "Tom",
    Age = 100,
    Gender = Gender.Male,
    Hobby = new List<string>() { "Game", "Sing" }
};

str = STJson.Serialize(stu);
Console.WriteLine(str);
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{"test_name":"Tom","Hobby":["Cooking","Sports"]}

STJsonSetting

STJsonSetting用于在序列化或者反序列化中添加一些个性化设置。原本设置是全局的。但随着一些设置项的增多,作者认为全局设置粘性太高,所以独立出来STJsonSetting用于解耦。同时独立的设置类也可以方便后续版本的功能扩展,当然后续版本的概率不是很大。除非作者不想当咸鱼了。

var stu = new Student() {
    Name = "Tom",
    Age = 100,
    Gender = Gender.Male,
    Hobby = new List<string>() { "Game", "Sing" }
};
STJsonSetting setting = new STJsonSetting();
setting.EnumUseNumber = true;
setting.IgnoreAttribute = true;
setting.Mode = STJsonSettingKeyMode.Exclude;
setting.KeyList.Add("Age");
str = STJson.Serialize(stu, setting);
Console.WriteLine(STJson.Format(str));
STJson.Deserialize<Student>(str);
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "test_name": "Tom",
    "Gender": 0,
    "Hobby": [
        "Game", "Sing"
    ]
}

Attribute的优先级大于STJsonSetting

JSON5

STJson(3.0)中提供了JSON5的支持,并且作者提供了一些更加便捷的想法,以下列文本为例:

json5.txt
{
    "normal_key": "This is a normal key and value.", // 这是一个行注释。
    /*
       这是一个块注释。
    */
    str_value_1: 'string_1',        // 对于键来说["]并不是必须的。、
    str_value_2: "string_2",        // 在所有的字符串中你可以选择使用[']或者["]。
    int_numbers: [                  // 支持16进制数字
        0x123, -0x123, +123, -123
    ],
    'float_numbers':[
        .123, 123., -.123, +123., +123E-2
    ],
    // 字符串续行 -> https://json5.org
    string_1: "string_1.\

string_2.\
    |<- some space",

    // 但是在STJson中你可以直接使用[\r\n]换行,且保留换行符。
    string_2: "string_1.
string_2.
        |<- some space",

    // 甚至这样使用。作者认为JSON5的续行方式不友好。
    // 通过[\]换行后,新行前面不能有空白,不然会被解析到字符串中。
    // 所以STJson允许连续字符串,并最终将其合并为一个字符串。
    string_3:
            "string_1."
            "string_2."
            "\r\nstring_3.",
    array:[
        123,true,"string_1.""string_2.","string_3",
    ],
}
var str_file = "./json5.txt";
var str_json = File.ReadAllText(str_file, Encoding.UTF8);
var json = STJson.Deserialize(str_json);
Console.WriteLine(json.ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "normal_key": "This is a normal key and value.",
    "str_value_1": "string_1",
    "str_value_2": "string_2",
    "int_numbers": [
        291,
        -291,
        123,
        -123
    ],
    "float_numbers": [
        0.123,
        123,
        -0.123,
        123,
        1.23
    ],
    "string_1": "string_1.string_2.    |<- some space",
    "string_2": "string_1.\r\nstring_2.\r\n        |<- some space",
    "string_3": "string_1.string_2.\r\nstring_3.",
    "array": [
        123,
        true,
        "string_1.string_2.",
        "string_3"
    ]
}

STJsonReader

STJsonReader可从一个TextReader中获取字符并动态解析,通常用于解析大文本数据。如:通过StreamReader从文件中或数据流中加载一个Json数据。

string str_json = @"
{
    name: 'DebugST',
    language: ['C#', 'JS'],
    address: {
        country: 'China',
        province: 'GuangDong',
        city: 'ShenZhen'
    }
}";
using (var reader = new STJsonReader(new StringReader(str_json))) {
    foreach (var v in reader) {
        //Console.WriteLine(STJson.Serialize(v, 4));
        Console.WriteLine(v.Path + ": " + v.Text + " - [" + v.ValueType + "]");
    }
}
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
name: DebugST - [String]
language: [...] - [Array]
language[0]: C# - [String]
language[1]: JS - [String]
address: {...} - [Object]
address.country: China - [String]
address.province: GuangDong - [String]
address.city: ShenZhen - [String]

默认情况下STJsonReader递归所有的数据。稍微改一下代码:

foreach (var v in STJson.Read(new StringReader(str_json))) {
    if (v.Path == "language") {
        Console.WriteLine(v.GetSTJson().ToString(4));
    } else {
        Console.WriteLine(v.Path + ": " + v.Text + " - [" + v.ValueType + "]");
    }
}
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
name: DebugST - [String]
[
    "C#",
    "JS"
]
address: {...} - [Object]
address.country: China - [String]
address.province: GuangDong - [String]
address.city: ShenZhen - [String]

GetSTJson()将从当前位置开始向后不停获取数据,直到获取到一个完整的STJson对象停止。并且如你所见,在之后的迭代中,已经跳过了language,因为他被GetSTJson()取走了。

STJsonReader性能远低于STJson.Deserialize(+n),且暂时没有优化STJsonReader的打算。作者认为STJsonReader仅在特殊场景下使用,而特殊场景不考虑性能。

并且由于STJsonReader采用的是动态解析,而且STJson并非采用的状态机进行解析,作者并不打算做严格的Json数据格式校验。所以下面的代码同样能正常运行,且与上面的str_json等效。

string str_json = @"
{
    name 'DebugST'
    language: ['C#' 'JS'],
    address: {
        country: 'China',,,,,,,
        province: 'GuangDong',,,,
        city:::::::::::: 'ShenZhen'";
string str_json = @"
{
    name: 'DebugST',
    language: ['C#', 'JS'],
    address: {
        country: 'China',,,,,,,
        province: 'GuangDong',,,,
        city:::::::::::: 'ShenZhen'
    }}}}}}}}}}
}1234567890";

STJsonReader在动态解析时,作者为了方便会忽略:,,这样在读取一个键值对的时候只需要读取两个token即可。且直接将第一个作为key,第二个作为value。部分源码如下:

private STJsonToken GetNextFilteredToken()
{
    foreach (var v in m_token_reader) {
        switch (v.Type) {
            case STJsonTokenType.KVSplitor:     // :
            case STJsonTokenType.ItemSplitor:   // ,
                continue;
            default:
                return v;
        }
    }
    return STJsonToken.None;
}

private STJsonReaderItem GetNextObjectKV()
{
    var token_key = this.GetNextFilteredToken();
    switch (token_key.Type) {
        case STJsonTokenType.None:
            return null;
        case STJsonTokenType.ObjectEnd:
            this.PopStack();
            return this.GetNextItem();
        case STJsonTokenType.Symbol:
        case STJsonTokenType.String:
            break;
        default:
            throw new STJsonParseException(token_key);
    }
    m_current_stack.Key = token_key.Value;
    var token_val = this.GetNextFilteredToken();
    if (token_val.Type == STJsonTokenType.None) {
        return null;
        //throw new Exception("error");
    }
    var item = new STJsonReaderItem(this, token_val)
    {
        ParentType = STJsonValueType.Object,
        Key = token_key.Value,
        Text = token_val.Value
    };
    return this.CheckValueToken(item, token_val);
}

private STJsonReaderItem GetNextArrayItem(){...}

STJsonWriter

STJsonWriterSTJsonReader起到相反的作用,STJsonWriter可以用于直接向一个TextWriter或数据流实时构造并写入Json字符串。

StringWriter sw = new StringWriter();
//using (var writer = new STJsonWriter(sw)) {
//    writer.StartWithArray((w) =>
//    {
//        // writer == w
//    });
//}
STJson.Write(sw, 4).StartWithArray((w) =>
{
    for (int i = 0; i < 1; i++) {
        Console.WriteLine("Level:" + w.Level);
        w.CreateObject(() =>
        {
            Console.WriteLine("Level:" + w.Level);
            w
            .SetItem("name", "DebugST")
            //.SetItem("language", STJson.CreateArray("C#", "JS"))
            //.SetItem("language", new string[] { "C#", "JS" })
            .SetArray("language", () =>
            {
                Console.WriteLine("Level:" + w.Level);
                w
                .Append("C#")
                .Append("JS");
            })
            .SetObject("address", () =>
            {
                Console.WriteLine("Level:" + w.Level);
                w
                .SetItem("country", "china")
                .SetItem("province", "GuangDong")
                .SetItem("city", "ShenZhen");
            });
        });
    }
});
Console.WriteLine(sw.ToString());
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
Level:1
Level:2
Level:3
Level:3
[
    {
        "name": "DebugST",
        "language": [
            "C#",
            "JS"
        ],
        "address": {
            "country": "china",
            "province": "GuangDong",
            "city": "ShenZhen"
        }
    }
]

STJsonWriter采用层层回调的方式,之所以采用这种方式是为了可以与目标Json的数据结构层次关系保持一致。如上所示,STJsonWriter内部自身管理着回调函数的调用层级关系。

json_src

在接下来的教程中我们会使用到一些测试数据,数据如下:

test.json
[{
    "name": "Tom", "age": 16, "gender": 0,
    "hobby": [
        "cooking", "sing"
    ]
},{
    "name": "Tony", "age": 16, "gender": 0,
    "hobby": [
        "game", "dance"
    ]
},{
    "name": "Andy", "age": 20, "gender": 1,
    "hobby": [
        "draw", "sing"
    ]
},{
    "name": "Kun", "age": 26, "gender": 1,
    "hobby": [
        "sing", "dance", "rap", "basketball"
    ]
}]

将其加载到程序中:

var json_src = STJson.Deserialize(System.IO.File.ReadAllText("./test.json"));

之后的案例中出现json_src则为以上对象。

STJsonPath

在源码STJsonExtension.cs中对STJson的功能进行了扩展,里面集成一些STJsonPath的功能。所以在STJson的原始代码中并没有对STJsonPath的依赖,STJson可独立使用。但STJsonPath作为STJson的辅助类,需依赖STJson

选择器

tokennote
$根节点选择器,可视作代表根节点对象。
@当前元素选择器,在遍历过程中指代当前被遍历的元素。
*通配符,表示可以代表任何一个节点。
.<name>子节点选择器,指定子节点的key
..深度选择器,表示可以是任意路径。
['<name>'(,'<name>')]列表选择器,指定子节点的key集合。
[<number>(,(number))]列表选择器,指定子节点的index集合。
[start:end:step]切片选择器,用于指定索引区间。
[(<expression>)]表达式选择器,用于输入一个运算表达式,并将结果作为索引继续向下选择。
[?(<expression>)]表达式选择器,用于输入一个运算表达式,并将结果转换为布尔值,决定是否继续选择。

使用方式

通过以下方式可以构建一个STJsonPath

// var jp = new STJsonPath("$[0]name");
// var jp = new STJsonPath("$[0].name");
var jp = new STJsonPath("[0]'name'"); // 以上方式均可以使用 $不是必须的
Console.WriteLine(jp.Select(json_src));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
["Tom"]

当然在STJson中的扩展函数中已经集成STJsonPath,可以通过下面的方式直接使用:

// var jp = new STJsonPath("[0].name");
// Console.WriteLine(json_src.Select(jp));
Console.WriteLine(json_src.Select("[0].name")); // 内部动态构建 STJsonPath
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
["Tom"]

STJsonPath以数组的方式返回数据,其返回值是STJson而不是List<STJson>STJson也可以是数组对象。

$开头对于STJsonPath来说并不是必须的,且内部会移除掉开头的$或者@$@仅在表达式中作为对象的变量使用。

在表达式中选择器的返回值仅返回选中的第一个结果。而不是数组列表,这点将在后面说明。

STJsonPath中允许使用'或者",比如:'a.b' "a.b" STJsonPath会将其视为一个独立的个体。而不是两个。列如有如下Json

{
    "a.b": "this is a test"
}

很明显通过Select("a.b")是无法获取到数据的,需要通过Select("'a.b'")

string strTemp = "{\"a.b\": \"this is a test\"}";
var json_temp = STJson.Deserialize(strTemp);
Console.WriteLine(json_temp.Select("a.b"));
Console.WriteLine(json_temp.Select("'a.b'"));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[]
["this is a test"]

在字符串中支持\进行转义:\r\n\t\f\b\a\v\0\x..\u....\.

通配符

通配符可表示当前层级中的任何一个节点。获取所有人员姓名。

Console.WriteLine(json_src.Select("*.name").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    "Tom", "Tony", "Andy", "Kun"
]

深度选择器

深度选择器与通配符类似,但深度选择器可以是任意层级。

Console.WriteLine(json_src.Select("..name").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    "Tom", "Tony", "Andy", "Kun"
]

列表选择器

列表选择器支持intstring两种方式,虽然在上面选择器的表格中列举了两个列表选择器,但是在STJsonPath中只有一个列表选择器,它们可以混合使用,比如下面的使用方式都是合法的:

STJsonPath在内部会自动拆分为两个列表选择器,并判断STJsonValueType决定使用哪个列表选择器。内部实现代码如下:

case STJsonPathItem.ItemType.List:
    if (jsonCurrent.ValueType == STJsonValueType.Object) {
        foreach (var v in item.Keys) {
            if (jsonCurrent[v] == null) {
                continue;
            }
            // ...
        }
    }
    if (jsonCurrent.ValueType == STJsonValueType.Array) {
        foreach (var v in item.Indices) {
            nIndexSliceL = v;
            if (nIndexSliceL < 0) nIndexSliceL = jsonCurrent.Count + nIndexSliceL;
            if (nIndexSliceL < 0) continue;
            if (nIndexSliceL >= jsonCurrent.Count) continue;
            // ...
        }
    }
    break;

选择索引为02的元素。

Console.WriteLine(json_src.Select("[0,2]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Tom",
        "age": 16,
        "gender": 0,
        "hobby": [
            "cooking", "sing"
        ]
    }, {
        "name": "Andy",
        "age": 20,
        "gender": 1,
        "hobby": [
            "draw", "sing"
        ]
    }
]

对于int索引可以使用负数,比如-1则表示获取最后一个元素。当STJsonPath检测到负数时候会执行STJson.Count - n将结果作为索引。

//Console.WriteLine(json_src.Select("-1").ToString(4));
Console.WriteLine(json_src.Select("[-1]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Kun",
        "age": 26,
        "gender": 1,
        "hobby": [
            "sing", "dance", "rap", "basketball"
        ]
    }
]

切片选择器

切片选择器用于在数组中选择一个片段,切片选择器默认值[0:-1:1],在切片选择器内部如下实现:

case STJsonPathItem.ItemType.Slice:
    if (jsonCurrent.ValueType != STJsonValueType.Array) {
        return;
    }
    if (nIndexSliceL < 0) nIndexSliceL = jsonCurrent.Count + nIndexSliceL;
    if (nIndexSliceR < 0) nIndexSliceR = jsonCurrent.Count + nIndexSliceR;
    if (nIndexSliceL < 0) nIndexSliceL = 0;
    else if (nIndexSliceL >= jsonCurrent.Count) nIndexSliceL = jsonCurrent.Count - 1;
    if (nIndexSliceR < 0) nIndexSliceR = 0;
    else if (nIndexSliceR >= jsonCurrent.Count) nIndexSliceR = jsonCurrent.Count - 1;
    if (nIndexSliceL > nIndexSliceR) {
        for (int i = nIndexSliceL; i >= nIndexSliceR; i -= item.Step) {
            // ...
        }
    } else {
        for (int i = nIndexSliceL; i <= nIndexSliceR; i += item.Step) {
            // ...
        }
    }
    break;

所以切片中的三个值等同于for循环中的三个条件,所以原理与效果就不再说明。

expressionrangenote
[::]0 <= R <= (OBJ).length - 1等同于*
[5:]5 <= R <= {OBJ}.length - 1从第6个元素开始,获取所有元素
[-1:0]{OBJ}.length - 1 >= R >= 0倒序获取数据
[0::2]0 <= R <= {OBJ}.length - 1顺序获取数据,且间隔一个数据

切片选择器中至少出现一个:step大于0,否则将获得异常。

Console.WriteLine(json_src.Select("[-1:]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Kun",
        "age": 26,
        "gender": 1,
        "hobby": [
            "sing", "dance", "rap", "basketball"
        ]
    }
]

表达式

[?()]中可支持下列运算符,优先级从上至下依次升高


operatornotee.g
re正则表达式[?(@.name re 'un')]
in左边的值或数组包含在右边的数组中[?(@.age in [16,20])]
nin左边的值或数组不包含在右边的数组中[?(@.hobby nin ['sing','draw'])]
anyof左边的值或数组和右边的数组存在交集[?(@.hobby anyof ['sing','draw'])]

表达式有两种模式:

过滤表达式

选中name中包含字母ku的元素:

//Console.WriteLine(json_src.Select("*.[?(@.name == 'kun')]").ToString(4));
Console.WriteLine(json_src.Select("*.[?(@.name re '(?i)ku')]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Kun",
        "age": 26,
        "gender": 1,
        "hobby": [
            "sing", "dance", "rap", "basketball"
        ]
    }
]

(?i)中的i表示忽略大小写,其正则表达式以.NetRegex为标准。(?...)开头则表示设置匹配模式。至于匹配模式自行查阅相关资料。

选中hobby不包含singswing的元素:

Console.WriteLine(json_src.Select("*.[?(@.hobby nin ['sing','draw'])]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Tony",
        "age": 16,
        "gender": 0,
        "hobby": [
            "game", "dance"
        ]
    }
]

普通表达式

普通表达式会将结果作为STJsonPath的部分继续匹配。

Console.WriteLine(json_src.Select("*.[('na' + 'me')]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    "Tom", "Tony", "Andy", "Kun"
]

[('na' + 'me')]'na' + 'me'的结果为'name',并且会将这个值作为索引,所以上述效果等同于*.name,当然返回值也可以是一个集合。

Console.WriteLine(json_src.Select("*.[(['na' + 'me', 'age', 0, 1 + 1])]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    "Tom", 16, "Tony", 16, "Andy", 20, "Kun", 26
]

上面表达式的结算结果值为['name', 'age', 0, 2]。但是很显然02将不会起到任何作用,因为第二层的数据对象并不是一个数组。

上面的表达式等同于*.['name', 'age', 0, 2]。如果将上面的换成第三层会得到下面的结果。

Console.WriteLine(json_src.Select("*.*.[(['na' + 'me', 'age', 0, 1 + 1])]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    "cooking", "game", "draw", "sing", "rap"
]

可以看到'name''age'对于hobby来说是无效的,因为hobby是一个数组。

测试表达式

可能读者并不了解表达式在内部是如何被执行了并且会输出什么样的结果,作者提供了一个静态测试函数TestExpression()可用于调试表达式。若有什么不明白的地方测试一下就会看到过程及结果。

Console.WriteLine(STJsonPath.TestExpression(
    null,           // [STJson] 用于替代表达式中出现的 $
    null,           // [STJson] 用于替代表达式中出现的 @
    "1+2+3"         // 表达式文本
    ).ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "type": "expression",
    "parsed": "{1 + 2 + 3}",        // 格式化后的文本 {}表示此部分需要单独执行 如: [1, {1+1}, 3]
    "polish": [
        "1", "2", " + ", "3", " + " // 逆波兰方式排列
    ],
    "steps": [                      // 执行步骤
        {
            "type": "excute",
            "operator": "+",
            "get_left_token": {     // 计算操作符左边元素的值,表达式左边也可能是一个表达式
                "parsed": "1",
                "type": "value",
                "result": {
                    "value_type": "Long",
                    "text": "1"
                }
            },
            "get_right_token": {
                "parsed": "2",
                "type": "value",
                "result": {
                    "value_type": "Long",
                    "text": "2"
                }
            },
            "result": {             // 该步骤执行结果
                "value_type": "Long",
                "text": "3"
            }
        }, {
            "type": "excute",
            "operator": "+",
            "get_left_token": {     // 此时操作符左边的元素为上一步的计算结果
                "parsed": "3",
                "type": "value",
                "result": {
                    "value_type": "Long",
                    "text": "3"
                }
            },
            "get_right_token": {
                "parsed": "3",
                "type": "value",
                "result": {
                    "value_type": "Long",
                    "text": "3"
                }
            },
            "result": {
                "value_type": "Long",
                "text": "6"
            }
        }
    ],
    "check_result": {               // 清空波兰表达式数据栈,确定最终输出结果。
        "parsed": "6",
        "type": "value",
        "result": {
            "value_type": "Long",
            "text": "6"
        }
    },
    "return": {                     // 最终返回值
        "value_type": "Long",
        "text": "6",
        "bool": true                // 如果用作布尔表达式则转换为 true
    }
}

如果过程不重要,仅仅是想看执行结果。

Console.WriteLine(STJsonPath.TestExpression(
    null,           // [STJson] 用于替代表达式中出现的 $
    null,           // [STJson] 用于替代表达式中出现的 @
    "1+2+3"         // 表达式文本
    ).SelectFirst("return").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "return": {                     // 最终返回值
        "value_type": "Long",
        "text": "6",
        "bool": true                // 如果用作布尔表达式则转换为 true
    }
}

内置函数

returnsignaturenote
stringtypeof(object)获取数据类型。
stringstr(object)转换为字符串。
stringupper(string)转换为大写。
stringlower(string)转换为小写。
longlen(stringarray)获取字符串或者数组长度。
longlong(stringnumber)转换为整数。
doubledouble(stringnumber)转换为浮点数。
numberabs(number)获取绝对值。
longround(number)四舍五入。
longceil(number)向上取整。
numbermax(array)求最大值。
numbermin(array)求最小值。
numberavg(array)求平均值。
numbersum(array)求总和。
stringtrim(string)裁切字符串两端的指定字符。
stringtrims(string)裁切字符串开始的指定字符。
stringtrime(string)裁切字符串末尾的指定字符。
string[]split(string)拆分字符串。
long or stringtime(+n)获取或格式化时间戳。

typeof具有下列返回值:

string long double boolean array object undefined

内置函数列表

随着版本的迭代更新(如果可能的话),内置函数可能随时发生变化,通过GetBuildInFunctionList()可以查看当前版本支持的内置函数信息。

Console.WriteLine(STJsonPath.GetBuildInFunctionList().ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "typeof",
        "demos": [
            "(object) -> typeof('abc')", "(array,bool) -> typeof(['abc',123],true)"
        ]
    },
    ...
]

读者可以通过TestExpression()按照demos对函数进行测试。

选中hobby长度大于2的元素:

Console.WriteLine(json_src.Select("..[?(len(@.hobby) > 1 + 1)]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Kun",
        "age": 26,
        "gender": 1,
        "hobby": [
            "sing", "dance", "rap", "basketball"
        ]
    }
]

自定义函数

在为表达式提供内置函数的时候作者也不知道开发者期望内置什么样的函数,随便内置了几个之后干脆不想了。没有什么问题是写代码解决不了的,没错。。。让开发者自己去写。但是开发者要怎么样为STJsonPath提供函数呢?

STJsonPath.CustomFunctions是一个静态字典。用于保存开发者自定义的函数。其函数签名如下:

public delegate STJson STJsonPathCustomFuncHander(STJsonPathExpFuncArg[] args);

public struct STJsonPathExpFuncArg
{
    public STJsonPathExpFuncArgType Type;
    public object Value;
}

public enum STJsonPathExpFuncArgType
{
    Undefined, Number, String, Boolean, Array, Object,
}

自定义函数返回值统一STJson,因为STJson可与STJsonPath完美衔接,在STJsonPath可直接使用选择器对自定义函数返回值进行操作。比如下面的案例为STJsonPath添加一个matches函数用于正则表达式操作。

STJsonPath.CustomFunctions.Add("matches", (objs) => {
    var json_ret = new STJson();
    json_ret.SetItem("count", 0);
    json_ret.SetItem("values", STJson.CreateArray());
    if (objs.Length != 2) {
        return json_ret;
    }
    var ms = Regex.Matches(objs[0].Value.ToString(), objs[1].Value.ToString());
    json_ret["count"].SetValue(ms.Count);
    foreach (Match v in ms) {
        json_ret["values"].Append(STJson.FromObject(new {
            success = v.Success,
            index = v.Index,
            length = v.Length,
            value = v.Value
        }));
    }
    return json_ret;
});

然后我们需要筛选出hobby中包含两个ao的元素:

Console.WriteLine(json_src.Select("*.hobby.*[?(matches(@,'a|o').count == 2)]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    "cooking", "basketball"
]

自定义函数优先级高于内置函数,也就是说如果在自定义函数中和内置函数中出现了同名函数,则优先调用自定义函数

表达式中的选择器

在表达式中的选择器仅返回选中的第一个结果,而不是数组列表。

Console.WriteLine(json_src[0].Select("name").ToString(4));
Console.WriteLine(STJsonPath.TestExpression(json_src[0], json_src[0], "@.name").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    "Tom"
]
{
    "type": "expression",
    "parsed": "{[@]['name']}",
    "polish": [
        "[@]['name']"
    ],
    "steps": [

    ],
    "check_result": {
        "parsed": "[@]['name']",
        "type": "selector",
        "root_json": "{\"name\":\"Tom\",\"age\":16,\"gender\":0,\"hobby\":[\"cooking\",\"sing\"]}",
        "current_json": "{\"name\":\"Tom\",\"age\":16,\"gender\":0,\"hobby\":[\"cooking\",\"sing\"]}",
        "selected_json": "[\"Tom\"]",
        "result": {                     // 返回值将只获取第一个结果
            "value_type": "String",
            "text": "Tom"
        }
    },
    "return": {
        "bool": true,
        "value_type": "String",
        "text": "Tom"
    }
}

选择方式

普通模式(默认方式):

Console.WriteLine(json_src.Select("..name", STJsonPathSelectMode.ItemOnly).ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    "Tom", "Tony", "Andy", "Kun"
]

路径模式:

Console.WriteLine(json_src.Select("..name", STJsonPathSelectMode.ItemWithPath).ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "path": [
            0, "name"
        ],
        "item": "Tom"
    }, {
        "path": [
            1, "name"
        ],
        "item": "Tony"
    }, {
        "path": [
            2, "name"
        ],
        "item": "Andy"
    }, {
        "path": [
            3, "name"
        ],
        "item": "Kun"
    }
]

保持结构:

Console.WriteLine(json_src.Select("..name", STJsonPathSelectMode.KeepStructure).ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Tom"
    }, {
        "name": "Tony"
    }, {
        "name": "Andy"
    }, {
        "name": "Kun"
    }
]

ParsedTokens

GetParsedTokens()用于获取当前STJsonPath得字符串在内部是如何被解析,且以STJson方式输出。如果你也想编写一个解析器,说不定可以给你提供一些思路。

Console.WriteLine(
    new STJsonPath("$..[?(matches(@.name,'u').count == 1)]").GetParsedTokens().ToString(2)
    );
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
  "type": "entry",
  "parsed": "[..]{matches({[@]['name']}, {'u'}) == 1}",
  "items": [
    {
      "type": "selector_item",
      "item_type": "Depth",
      "value": ".."
    }, {
      "type": "expression",
      "parsed": "{matches({[@]['name']}, {'u'}) == 1}",
      "items": [
        {
          "type": "function",
          "parsed": "matches({[@]['name']}, {'u'})",
          "name": "matches",
          "args": {
            "parsed": "({[@]['name']}, {'u'})",
            "items": [
              {
                "type": "expression",
                "parsed": "{[@]['name']}",
                "items": [
                  {
                    "type": "expression_item",
                    "item_type": "selector",
                    "items": [
                      {
                        "type": "selector_item",
                        "item_type": "Current",
                        "value": "@"
                      }, {
                        "type": "selector_item",
                        "item_type": "List",
                        "value": [
                          "name"
                        ]
                      }
                    ]
                  }
                ]
              }, {
                "type": "expression",
                "parsed": "{'u'}",
                "items": [
                  {
                    "type": "expression_item",
                    "item_type": "string",
                    "value": "u"
                  }
                ]
              }
            ]
          },
          "selector": {
            "parsed": "['count']",
            "items": [
              {
                "type": "selector_item",
                "item_type": "List",
                "value": [
                  "count"
                ]
              }
            ]
          }
        }, {
          "type": "expression_item",
          "item_type": "long",
          "value": 1
        }, {
          "type": "expression_item",
          "item_type": "operator",
          "value": "=="
        }
      ]
    }
  ]
}

STJson [高级应用]

获取值

试想一个场景,作为一个WEB后端服务,需要处理前端提交过来的Json数据,假定我们期望得到以下Json数据:

{
    "type": "get_list",
    "page": {
        "from": 100,
        "size": 10
    },
    "other": {}
}

并且我们已经将上面的数据转化为STJson且命名为json_post。其中fromsize用于翻页功能。那么后台可能会做如下判断:

if(json_post["page"] == null) { /* do something */ }
if(json_post["page"]["from"] == null) { /* do something */ }
if(json_post["page"]["from"].ValueType != STJsonValueType.Long) {
    /* do something */
}
int nFrom = json_post["page"]["from"].GetValue<int>();
// 或者 直接暴力一点
int nFrom = 0;
try{
    nFrom = json_post["page"]["from"].GetValue<int>();
}catch{
    /* do something */
}

很显然上面的代码让你抓狂。。。当然为了减少麻烦可以在后端代码中为其创建一个实体对象,然后将Json绑定到实体对象中。。。但是为每个Post过来的数据类型创建一个实体对象是不是也挺麻烦的。但是如果使用下面的代码。

int nFrom = json_post.GetValue<int>("page.from");

或许你会疑惑,,如果路径不存在或者from根本不是数字怎么办?emm....上面的代码依然会报错,因为上面的代码内部调用是:

public static T GetValue<T>(this STJson json, string strJsonPath) {
    return json.GetValue<T>(new STJsonPath(strJsonPath));
}

public static T GetValue<T>(this STJson json, STJsonPath jsonPath) {
    var j = jsonPath.SelectFirst(json);
    if (j == null) {
        throw new STJsonPathException("Can not selected a object with path {" + jsonPath.SourceText + "}");
    }
    var t = typeof(T);
    bool bProcessed = true;
    var convert = STJsonBuildInConverter.Get(t);
    if (convert != null) {
        var value = convert.JsonToObject(t, json, ref bProcessed);
        if (bProcessed) {
            return (T)value;
        }
    }
    return (T)j.Value;
}

因为当不存在元素时候,不知道需要返回什么值,可能读者认为无法获取值,返回一个默认值就好了,的确可以这么设计,可是调用者如何确定返回的值是真实的值还是默认值。无法确定是否异常了。除非强制指定一个默认值。

int nFrom = json_post.GetValue<int>("page.from", 0);

如果存在元素且能正常转换则返回元素值,否则返回0

或者想知道是否返回的是真实值:

int nFrom = 0;
var bFlag = json_src.GetValue<int>("page.from", out nFrom);

若填写的strJsonPath会获取到多个值,则取第一个值。

设置值

如果我们需要手动构造一个Json可以通过STJson.SetItem()来添加元素。可是有些场景这样使用似乎有点麻烦,比如以ElasticSearch数据库的query语法为例。我们需要构造一个下面的数据:

{
    "query":{
        "term":{
            "field_name": "field_value"
        }
    }
}

那么按照之前的写法我们需要这样去构造一个STJson对象:

var json = new STJson()
    .SetItem("query", new STJson()
        .SetItem("term", new STJson()
            .SetItem("field_name", "field_value"))
        );
// or
var json = STJson.FromObject(new {
    query = new {
        term = new {
            field_name = "field_value"
        }
    }
});

但是还可以这样写代码:

var json = new STJson().Set("query.term.field_name", "field_value");
Console.WriteLine(json.ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "query":{
        "term":{
            "field_name": "field_value"
        }
    }
}

甚至是这样:

var json = new STJson().Set("array[0:4].[key_1,key_2]", "value");
Console.WriteLine(json.ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "array": [
        {
            "key_1": "value",
            "key_2": "value"
        }, {
            "key_1": "value",
            "key_2": "value"
        }, {
            "key_1": "value",
            "key_2": "value"
        }, {
            "key_1": "value",
            "key_2": "value"
        }, {
            "key_1": "value",
            "key_2": "value"
        }
    ]
}

Set中仅仅支持列表选择器切片选择器。不支持其他选择器。

STJsonCreator

虽然上面通过Set也可以很容易的构造出一个Json数据,但是对于多层嵌套的数据结构通过Set也能实现,但是代码会变得过于复杂并且无法直观的看到Json数据的结构。STJsonCreatorSTJsonWriter功能类似,不过STJsonCreator是直接创建一个STJson对象,通常用于创建复杂结构的Json对象。依然使用ElasticSearch数据库为例,模拟一个数据检索。

var str_json_post = @"
{
    type: 'student',
    names: ['DebugST', 'None'],
}";                                         // 模拟前端提交的请求数据
var json_post = STJson.Deserialize(str_json_post);
var json_es_query = STJson.Create((c) =>    // 构造ES检索语法
{
    c
    .SetItem("from", (json_post.GetValue("page", 1) - 1) * json_post.GetValue("size", 10))
    .SetItem("size", json_post.GetValue("size", 10))
    .Set("query.bool", () =>
    {
        c
        .Set("filter", () =>
        {
            c.Set("term.type", json_post["type"]);
        })
        // c.Set(str_path, bool, callback) bool -> if true, create this path.
        .Set("should", !json_post["names"].IsNullOrNullValue(), () =>
        {
            // c.Append(0, 5, 1, (i)=>{ }) => for(int i = 0; i < 5; i+=1) {...}
            // c.Append(IEnumerable, callback);
            c.Append(json_post["names"], (item) => // => foreach(var item in json_post) {...}
            {
                c.Set("term.name", item);
            });
        });
    });
});
Console.WriteLine(json_es_query.ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "from": 0,
    "size": 10,
    "query": {
        "bool": {
            "filter": {
                "term": {
                    "type": "student"
                }
            },
            "should": [
                {
                    "term": {
                        "name": "DebugST"
                    }
                },
                {
                    "term": {
                        "name": "None"
                    }
                }
            ]
        }
    }
}

可以看到通过POST过来的请求很容易的就构造出了ES数据库的检索语法,并且Json的数据结构保持了一致。并且在SetAppend中提供了多重重载,可以更加便捷的进行逻辑处理,当然它们仅仅是语法糖。通过iffor也能达到同样的目的。

STJsonPath 回调函数

通过上面的Set我们是否能够将json_src中每个人的喜好都添加一个coding?。。似乎不太可以。毕竟向hobby中添加数据需要append而不是set

当然也并不是没有办法。

json_src.Select("*.hobby", (arg) => {
    arg.Json.Append("coding");
});
Console.WriteLine(json_src.ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Tom",
        "age": 16,
        "gender": 0,
        "hobby": [
            "cooking", "sing", "coding"
        ]
    },
    ...
]

Select中支持两种回调函数,一个需要返回值,另一个不需要返回值,如同上面的。而下面的带返回值。

var json = json_src.Select("*.hobby.*", (arg) => {
    return new STJsonPathCallBackResult() {
        Selected = true,            // 是否将此条 json 添加到结果中
        arg.Json = new STJson()         // 需要被添加到结果的 json
            .SetItem("path", arg.Path)
            .SetItem("item", arg.Json.Value.ToString().ToUpper())
    };
});
Console.WriteLine(json.ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "path": [
            0, "hobby", 0
        ],
        "item": "COOKING"
    },
    ...
]

PathItem

STJsonPath中有一个比较特殊的数据结构。

[
    {
        "path": [
            ...
        ],
        "item": ...
    },
    ...
]

可以通过RestorePathJson()还原其结构。

var json = json_src.Select("*.hobby.*", (arg) => {
    return new STJsonPathCallBackResult() {
        Selected = true,
        arg.Json = new STJson()
            .SetItem("path", arg.Path)
            .SetItem("item", arg.Json.Value.ToString().ToUpper())
    };
});
Console.WriteLine(STJsonPath.RestorePathJson(json).ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "hobby": [
            "COOKING", "SING"
        ]
    }, {
        "hobby": [
            "GAME", "DANCE"
        ]
    }, {
        "hobby": [
            "DRAW", "SING"
        ]
    }, {
        "hobby": [
            "SING", "DANCE", "RAP", "BASKETBALL"
        ]
    }
]

Clone

如果想向所有的用户喜好中添加coding。但是又不想影响源数据,那么可以克隆一份数据进行操作。

json_src.Clone().Select("*.hobby", (arg) => {
    arg.Json.Append("coding");
});
Console.WriteLine(json_src.ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Tom",
        "age": 16,
        "gender": 0,
        "hobby": [
            "cooking", "sing"
        ]
    },
    ...
]

数据聚合

STJson中内置了部分扩展函数用于聚合操作,进行一些简单的数据处理。结合STJsonPath可以快速且简单的完成一些数据操作。

sort

Sort()用于对数据进行排序,内部采用归并排序方式,并且Sort有多个重载。但最终版本的Sort签名如下,所有的重载,最终调此函数。

public static STJson Sort(this STJson json, bool is_new_instance, STJsonSortCallback callback);
var arr_obj = new object[] { 4, 2, 5, 6, 1, true, null, new { aa = "aa" } };
var json_objs = STJson.FromObject(arr_obj);
Console.WriteLine(json_objs.Sort());
Console.WriteLine(json_objs.Sort(true));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[null,1,2,4,5,6,true,{"aa":"aa"}]
[{"aa":"aa"},true,6,5,4,2,1,null]

你是否会感到疑惑为什么会是这样的结果?。。。但是在那之前为什么你不先疑惑一下为什么要用这样的数组去排序呢?如上所示Sort需要一个回调函数,STJson的扩展提供了两个默认的回调函数,它被如下代码构造:

private static STJsonSortCallback BuildDefaultSortCallback(bool is_desc)
{
    return (a, b) =>
    {
        int n_ret = 0;
        if (a.IsNullOrNullValue()) {
            n_ret = -1;
        } else if (b.IsNullOrNullValue()) {
            n_ret = 1;
        } else if (a.IsNumber && b.IsNumber) {
            n_ret = Convert.ToDouble(a.Value) < Convert.ToDouble(b.Value) ? -1 : 1;
        } else if (a.ValueType != b.ValueType) {
            n_ret = a.ValueType - b.ValueType;
        } else {
            switch (a.ValueType) {
                case STJsonValueType.Boolean:
                    n_ret = (bool)b.Value ? -1 : 1;
                    break;
                case STJsonValueType.Datetime:
                    n_ret = (DateTime)a.Value < (DateTime)b.Value ? -1 : 1;
                    break;
                case STJsonValueType.String:
                    n_ret = string.Compare(a.Value.ToString(), b.Value.ToString());
                    break;
            }
        }
        return is_desc ? -n_ret : n_ret;
    };
}

所以如果你没有那么奇怪的数组的话,那么一切都会正常起来。如果实在是想只对里面的数字排序怎么办?

var arr_obj = new object[] { 4, 2, 5, 6, 1, true, null, new { aa = "aa" } };
var json_objs = STJson.FromObject(arr_obj)
    .Select("..[?(typeof(@) in ['long', 'double'])]");
Console.WriteLine(json_objs.Sort());
Console.WriteLine(json_objs.Sort(true));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[1,2,4,5,6]
[6,5,4,2,1]

指定path进行排序:

Console.WriteLine(json_src.Sort("age").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    {
        "name": "Tony",
        "age": 16,
        "gender": 0,
        "hobby": [
            "game", "dance"
        ]
    }, {
        "name": "Tom",
        "age": 16,
        "gender": 0,
        "hobby": [
            "cooking", "sing"
        ]
    }, {
        "name": "Andy",
        "age": 20,
        "gender": 1,
        "hobby": [
            "draw", "sing"
        ]
    }, {
        "name": "Kun",
        "age": 26,
        "gender": 1,
        "hobby": [
            "sing", "dance", "rap", "basketball"
        ]
    }
]

当然你也可以指定降序排列:

Console.WriteLine(json_src.Sort("age", true).ToString(4));

group

Group()用于对数据指定path进行分组。使用方式如下:

Console.WriteLine(json_src.Group("gender").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "gender": [
        {
            "value": 0,
            "items": [
                {
                    "name": "Tom",
                    "age": 16,
                    "gender": 0,
                    "hobby": [
                        "cooking", "sing"
                    ]
                }, {
                    "name": "Tony",
                    "age": 16,
                    "gender": 0,
                    "hobby": [
                        "game", "dance"
                    ]
                }
            ]
        }, {
            "value": 1,
            "items": [
                {
                    "name": "Andy",
                    "age": 20,
                    "gender": 1,
                    "hobby": [
                        "draw", "sing"
                    ]
                }, {
                    "name": "Kun",
                    "age": 26,
                    "gender": 1,
                    "hobby": [
                        "sing", "dance", "rap", "basketball"
                    ]
                }
            ]
        }
    ]

同样的你也可以使用多个path进行分组,但是和Sort不同的是这些path不是对第一个path的结果再进行分组。是并列的,而非嵌套的。

Console.WriteLine(json_src.Group("gender", "age").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "gender": [...],
    "age": [...]
}

terms

Terms()ElasticSearch数据库中的聚合类似,统计某个字段的出现次数,与Sort()一样,它可以选择是否指定path或者多个path

Console.WriteLine(json_src.Terms("hobby", "gender").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "hobby": [
        {
            "value": "cooking",
            "count": 1
        }, {
            "value": "sing",
            "count": 3
        }, {
            "value": "game",
            "count": 1
        }, {
            "value": "dance",
            "count": 2
        }, {
            "value": "draw",
            "count": 1
        }, {
            "value": "rap",
            "count": 1
        }, {
            "value": "basketball",
            "count": 1
        }
    ],
    "gender": [
        {
            "value": 0,
            "count": 2
        }, {
            "value": 1,
            "count": 2
        }
    ]
}

如你所见,似乎hobby并没有对count进行排序啊?是的。。。这是作者故意的。。。咋地?不服?。。。有没有可能其实Terms()还有一个作用?在ES数据库中有个cardinality。用作统计某字段去重后的数据个数。

有没有可能其实作者也想实现一个这样的函数?。。但是这个函数写到一半的时候,作者当场Delete。。。没必要啊。。上面的Terms()不就已经完成了这个工作了吗?比如:

json_src.Terms("hobby")["hobby"].Count;

而且有没有一种可能cardinality只统计个数。。。但是。。如果想要获取到去重后的字段值都有哪些要怎么办?。。。而刚才说的这些情况Terms()似乎都已经完成了。。仅仅是没有做排序。都已经有Sort()函数了,再单独排个序怎么了?

json_src.Terms("hobby").ForEach((item)=>item.Sort("count", true));

如果想获得人员最受欢迎的前三个喜好要怎么做?

Console.WriteLine(
    json_src.Terms("hobby")["hobby"]
    .Sort("count", true)
    .Select("[0:3]")
    .Select("*.value")
    .ToString(4)
    );
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
[
    "sing", "dance", "basketball"
]

min, max

Min() Max()分别为寻找最小值和最大值元素,可以不指定或者指定多个path

Console.WriteLine(json_src.Min("age").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "age": {
        "count": 4,                 // 其中 4 个元素参与了计算
        "value": 16,                // 最小的 age 值为 16
        "items": [                  // 满足最小值的元素
            {
                "name": "Tom",
                "age": 16,
                "gender": 0,
                "hobby": [
                    "cooking", "sing"
                ]
            }, {
                "name": "Tony",
                "age": 16,
                "gender": 0,
                "hobby": [
                    "game", "dance"
                ]
            }
        ]
    }
}

其实上面的效果有点像Group有点像了,再加上一个Sort()。。简直就是一模一样。。。连作者自己都懵逼了。。。就应该在Min/Max()内部使用Group() + Sort()实现的。。算了算了。。代码都写完了。但是很显然Sort()必然会降低效率。。但是话又说回来。。小数据量不在乎效率。。但是大数据量呢???大数据量你给我说你用Json数组保存????你食不食油饼???

avg, sum

Avg() Sum()分别用于计算平均值和汇总,可以不指定或者指定多个key

Console.WriteLine(json_src.Avg("age").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "age": {
        "count": 4,         // 其中 4 个元素参与了计算
        "value": 19.5       // 平均值 19.5
    }
}

Sum()返回值与Avg()一致,唯一不同的是,value一个是平均值,一个是汇总。

STJsonPath.Name

在上面的演示中我们有使用指定一个字段进行数据操作,在结果的json中会以这个字段名称作为一个key输出。但是上面并没有强调是在指定key进行操作,而是path。只是路径比较简单让我们看起来像是key

如果这样进行聚合呢?

Console.WriteLine(json_src.Terms("hobby[0]").ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "hobby[0]": [
        {
            "value": "cooking",
            "count": 1
        }, {
            "value": "game",
            "count": 1
        }, {
            "value": "draw",
            "count": 1
        }, {
            "value": "sing",
            "count": 1
        }
    ]
}

好像看上去很奇怪的样子。其实STJsonPath有一个Name属性。

Console.WriteLine(json_src.Terms(new STJsonPath("test", "hobby[0]")).ToString(4));
/*******************************************************************************
 *                                [output]                                     *
 *******************************************************************************/
{
    "test": [
        {
            "value": "cooking",
            "count": 1
        }, {
            "value": "game",
            "count": 1
        }, {
            "value": "draw",
            "count": 1
        }, {
            "value": "sing",
            "count": 1
        }
    ]
}

THE END

非常感谢看到最后,如果在使用过程中遇到什么问题请一定及时联系作者,作者一定在第一时间用小本本记录下来。至于改不改。。。日后再说。。。

  • TG: DebugST
  • QQ: 2212233137
  • Mail: 2212233137@qq.com
STLib.Json 简介 路劲疑问 表达式疑问 STJson API 数据类型关系 静态函数 非静态函数 扩展函数 字段 索引器 其他对象 STJson [基本应用] object -> string string -> object STJson -> object STJsonConverter STJsonAttribute STJsonSetting JSON5 STJsonReader STJsonWriter json_src STJsonPath 选择器 使用方式 通配符 深度选择器 列表选择器 切片选择器 表达式 过滤表达式 普通表达式 测试表达式 内置函数 内置函数列表 自定义函数 表达式中的选择器 选择方式 ParsedTokens STJson [高级应用] 获取值 设置值 STJsonCreator STJsonPath 回调函数 PathItem Clone 数据聚合 sort group terms min, max avg, sum STJsonPath.Name THE END