Json 序列化


  Json (JavaScript Object Notation) 是一种轻量级的数据交换格式,由于它易于阅读和编写,同时也易于机器解析和生成,因此正成为一种被广泛应用的数据格式。 目前比较流行的Json序列化类库是 Newtonsoft.Json,Fireasy 基本上能够替代它。

  Fireasy 的 Json 序列化支持的数据范围有:DataSetDataTableDataRowIEnumerableIDictionary 以及 StringDateTime 等简单类型、dynamic 动态类型。

  JsonSerializer 类有 Serialize 和 Deserialize 两个方法。为了方便演示,我们先定义类型,如下所示:

public class JsonData
{
    public string Name { get; set; }

    public DateTime Birthday { get; set; }

    public decimal? Age { get; set; }

    public List<WorkRecord> WorkRecords { get; set; }
}

public class WorkRecord
{
    public string Company { get; set; }

    public DateTime StartDate { get; set; }

    public DateTime? EndDate { get; set; }
}

1、序列化选项

  JsonSerializeOption 类提供了很多的选项,用于在序列化和反序列化的控制。


  • InclusiveNames 和 InclusiveMembers 属性

  指定只允许包含的属性数组。这两个属性指定序列化时只允许输出的属性,一个是 string 数组,另一个是 System.Reflection.MemberInfo 数组。如下所示:

[TestMethod]
public void TestInclusiveNames()
{
    var option = new JsonSerializeOption { InclusiveNames = new string[] { "Name" } };
        
    var serializer = new JsonSerializer(option);

    var obj = new JsonData
        {
            Age = 12,
            Name = "fireasy",
            Birthday = DateTime.Today
        };

    var json = serializer.Serialize(obj);
        
    Console.WriteLine(json);
}

  输出的 Json 文本如下所示:

{ "Name": "fireasy" }

  • ExclusiveNames 和 ExclusiveMembers 属性

  指定需要排除的属性数组。这两个属性指定序列化时需要忽略的属性,一个是 string 数组,另一个是 System.Reflection.MemberInfo 数组。如下所示:

[TestMethod]
public void TestExclusiveNames()
{
    var option = new JsonSerializeOption { InclusiveNames = new string[] { "Birthday" } };
        
    var serializer = new JsonSerializer(option);

    var obj = new JsonData
        {
            Age = 12,
            Name = "fireasy",
            Birthday = DateTime.Today
        };

    var json = serializer.Serialize(obj);
        
    Console.WriteLine(json);
}

  输出的 Json 文本如下所示:

{ "Name": "fireasy", "Age": 12, "WorkRecords": null }

  • Include 和 Exclude 方法

  这两个方法是辅助方法,用于从指定的 lambda 表达式中解析出 InclusiveMembers 或 ExclusiveMembers 属性。如下所示:

[TestMethod]
public void TestInclude()
{
    var option = new JsonSerializeOption()
        .Include<JsonData>(s => s.Name)
        .Include<JsonData>(s => s.Age)
        .Include<JsonData>(s => s.WorkRecords)
        .Exclude<WorkRecord>(s => s.StartDate)
        .Exclude<WorkRecord>(s => s.EndDate);
                
    option.Indent = true;
        
    var serializer = new JsonSerializer(option);

    var obj = new JsonData
        {
            Age = 12,
            Name = "fireasy",
            Birthday = DateTime.Today,
            WorkRecords = new List<WorkRecord> 
            { 
                new WorkRecord { Company = "zeek", StartDate = DateTime.Parse("2000-09-01") } 
            }
        };

    var json = serializer.Serialize(obj);
        
    Console.WriteLine(json);
}

  输出的 Json 文本如下所示:

{
  "Name": "fireasy",
  "Age": 12,
  "WorkRecords": [
    {
      "Company": "zeek"
    }
  ]
}

  • Indent 属性

  控制输出的 Json 要不要换行并按层次缩进,默认为 false。


  • NamingHandling 属性

  用于设置属性的命名方式,有 Default 和 Camel 两种,如果设置为 Camel 后将按驼峰命名。如下所示:

[TestMethod]
public void TestNaming()
{
    var option = new JsonSerializeOption();
    option.NamingHandling = NamingHandling.Camel;
        
    var serializer = new JsonSerializer(option);

    var obj = new JsonData
        {
            Age = 12,
            Name = "fireasy",
            Birthday = DateTime.Today
        };

    var json = serializer.Serialize(obj);
        
    Console.WriteLine(json);
}

  输出的 Json 文本如下所示:

{ "name": "fireasy", "age": 12, "birthday": "2020-09-20", "workRecords": null }

  • NullValueHandling 属性

  用于设置属性为空时,是否输出 null,如果设置为 Ignore 后将忽略为 null 的属性。默认为输出 null。如下所示:

[TestMethod]
public void TestNaming()
{
    var option = new JsonSerializeOption();
    option.NullValueHandling = NullValueHandling.Ignore;
        
    var serializer = new JsonSerializer(option);

    var obj = new JsonData
        {
            Age = 12,
            Name = "fireasy",
            Birthday = DateTime.Today
        };

    var json = serializer.Serialize(obj);
        
    Console.WriteLine(json);
}

  输出的 Json 文本如下所示:

{ "Name": "fireasy", "Age": 12, "Birthday": "2020-09-20" }

  • ReferenceLoopHandling 属性

  用于控制当发生循环引用时该如何处理。默认不做检查,这样会提升一定的性能,但是当发生循环引用时,将会导致堆栈溢出。如果设置为 Error 将抛出异常,设置为 Ignore 将会终止循环处理,但不会抛出异常。


  • DateFormatHandling 属性

  用于设置日期类型的输出格式。取值有 Default、IsoDateFormat 和 MicrosoftDateFormat。


  • DateTimeZoneHandling 属性

  用于设置时间的时区属性。取值有 Local、Utc 和 Unspecified。


  • KeyHandling 属性

  用于设置输出的 json 中属性名称是否需要加上双引号。


  • Converters 属性

  指定多个转换器,转换器将在下一小节进行详细说明。


2、转换器

  转换器的基类是 JsonConverter 类,它用于处理特定类型的序列化和反序列化过程。

  • CanConvert 方法: 用于判断的类型是否可应用于此转换器。

  • CanWrite 属性: 表示此转换器是否可序列化对象。

  • CanRead 属性: 表示此转换器是否可反序列化对象。

  • WriteJson 方法: 处理对象的序列化。

  • ReadJson 方法: 处理对象的反序列化。

  下面是一个日期类型的转换器,它支持自定义日期格式:

public class DateTimeJsonConverter : JsonConverter
{
    public DateTimeJsonConverter(string formatter = "yyyy-MM-dd")
    {
        Formatter = formatter;
    }

    public string Formatter { get; set; }

    public override bool CanConvert(Type type)
    {
        return type == typeof(DateTime) ||
            type == typeof(DateTime?);
    }

    public override void WriteJson(JsonSerializer serializer, JsonWriter writer, object obj)
    {
        var value = (DateTime?)obj;
        if (value == null)
        {
            writer.WriteNull();
        }
        else
        {
            writer.WriteString(value.Value.ToString(Formatter, CultureInfo.CurrentCulture));
        }
    }

    public override object ReadJson(JsonSerializer serializer, JsonReader reader, Type dataType)
    {
        var json = reader.ReadRaw();

        if (DateTime.TryParseExact(json, Formatter, 
            CultureInfo.CurrentCulture.DateTimeFormat, 
            DateTimeStyles.None, out DateTime time))
        {
            return time;
        }

        return null;
    }
}

  将 DateTimeJsonConverter 添加到 JsonSerializeOption 对象的 Converters 属性里,如下所示:

[TestMethod]
public void TestConverter()
{
    var option = new JsonSerializeOption();
    option.Converters.Add(new DateTimeJsonConverter("yyyy-m-d h:m"));
        
    var serializer = new JsonSerializer(option);

    var obj = new JsonData
        {
            Age = 12,
            Name = "fireasy",
            Birthday = DateTime.Now
        };

    var json = serializer.Serialize(obj);
        
    Console.WriteLine(json);
}

  输出的 Json 文本如下所示:

{ "Name": "fireasy", "Age": 12, "Birthday": "2020-9-20 20:45" }

  JsonConverter 类里会使用到 JsonWriterJsonReader 对象,它们的方法见下一小节。

  转换器除了在 JsonSerializeOption 对象里指定外,还可以使用 TextConverterAttribute 特性在类型上标记,以及使用 TextPropertyConverterAttribute 特性在属性上标记。如下所示:

[TextConverter(typeof(JsonDataConverter))]
public class JsonData
{
    public string Name { get; set; }

    public DateTime Birthday { get; set; }

    public decimal? Age { get; set; }

    [TextPropertyConverter(typeof(WorkRecordConverter))]
    public List<WorkRecord> WorkRecords { get; set; }
}

public class JsonDataConverter : JsonConverter<JsonData>
{
}

public class WorkRecordConverter : JsonConverter<WorkRecord>
{
}

3、Json 编写器

  JsonWriter 类用于向一个 TextWriter 对象里写入 json 文本。以下是它的常用方法:

  • WriteNull 方法: 输出一个 null。

  • WriteValue 方法: 输出一个任意类型的值。

  • WriteRaw 方法: 输出一个对象或集合的 json,它可能是一段 { }[ ]

  • WriteString 方法: 输出一个 string 对象。

  • WriteKey 方法: 输出一个属性名。

  • WriteComma 方法: 输出一个冒号。

  • WriteStartObject 、WriteEndObject 方法: 分别输出 {}

  • WriteStartArray 、WriteEndArray 方法: 分别输出 []


4、Json 读取器

  JsonReader 类用于读取一个 TextReader 对象里的 json 文本。以下是它的常用方法:

  • ReadRaw 方法: 读取一个对象或集合的 json,它可能是一段 { }[ ]

  • SkipWhiteSpaces 方法: 跳过后面的连续空格或换行符。

  • ReadValue 方法: 读取一个任意类型的值,这些值可能是 intstringboolDateTime 类型。

  • ReadAs* 序列方法: 读取指定类型的值,如 ReadAsInt32、ReadAsString 等等。

  • ReadKey 方法: 读取一个属性名。

  • IsDelimiter 方法: 判断字符是否为界定符。界定符有 {}[]: 和 空格。

  • IsNextCharacter 方法: 判断下一个字符是否与指定的字符相同。

  • IsWhiteSpace 方法: 判断指定的字符是否为空格。

  • IsNull 方法: 判断是否为 null。

  • AssertAndConsume 方法: 判断下一个字符是否与指定的字符相同,如果不相同则抛出异常。

  • AssertNextIsDelimiterOrSeparator 方法: 判断下一个字符是否是指定结束的字符,如果不相同则抛出异常。


5、动态类型与匿名类型

  Fireasy 反序列化动态类型的机制与 Newtonsoft.Json 是不一样的,Fireasy 反序列化的对象是 dynamic 而不是 JTokenJValue 等等,因此在访问该对象时要方便得多。如下所示:

[TestMethod]
public void TestDeserializeDynamic()
{
    var json = @"
[{
  ""Name"": ""fireasy"",
  ""Age"": 12,
  ""WorkRecords"": [
    {
      ""Company"": ""zeek""
    },
    {
      ""Company"": ""book""
    }
  ]
}]";

    var serializer = new JsonSerializer();
    var array = serializer.Deserialize<dynamic>(json);
        
    foreach (var item in array)
    {
        foreach (var record in item.WorkRecords)
        {
            Console.WriteLine(record.Company);
        }
    }
}

  Fireasy 反序列化匿名类型时,需要指定一个匿名对象,以标明其结构。如下所示:

[TestMethod]
public void TestDeserializeAnonymous()
{
    var json = @"
{
  ""Name"": ""fireasy"",
  ""Age"": 12,
  ""WorkRecords"": [
    {
      ""Company"": ""zeek""
    },
    {
      ""Company"": ""book""
    }
  ]
]";

    var serializer = new JsonSerializer();
    var obj = serializer.Deserialize(json, new 
    { 
        Name = "", 
        Age = 0, 
        WorkRecords = new List<dynamic> { new { Company = "" } } 
    });
        
    foreach (var record in obj.WorkRecords)
    {
        Console.WriteLine(record.Company);
    }
}

6、自定义属性名称

  你可以使用 TextSerializeElementAttribute 特殊为属性标记输出的属性名称,而不需要修改属性名称。如下所示:

public class JsonData
{
    [TextSerializeElement("name")]
    public string Name { get; set; }

    [TextSerializeElement("bir_date")]
    public DateTime Birthday { get; set; }

    public decimal? Age { get; set; }

    [TextSerializeElement("records")]
    public List<WorkRecord> WorkRecords { get; set; }
}

[TestMethod]
public void TestSerializeElement()
{
    var option = new JsonSerializeOption();
    option.Indent = true;
    var serializer = new JsonSerializer(option);

    var obj = new JsonData
        {
            Age = 12,
            Name = "fireasy",
            Birthday = DateTime.Today,
            WorkRecords = new List<WorkRecord> 
            { 
                new WorkRecord { Company = "zeek" } 
            }
        };

    var json = serializer.Serialize(obj);
        
    Console.WriteLine(json);
}

  输出的 Json 文本如下所示:

{
  "name": "fireasy",
  "Age": 12,
  "bir_date": "2020-09-20",
  "records": [
    {
      "Company": "zeek"
    }
  ]
}

7、自定义输出格式

  TextFormatterAttribute 特性为 decimalDateTime 类型的属性指定输出格式,如下所示:

public class JsonData
{
    public string Name { get; set; }

    [TextFormatter("yyyy年m月d日")]
    public DateTime Birthday { get; set; }

    [TextFormatter("n")]
    public decimal? Age { get; set; }

    public List<WorkRecord> WorkRecords { get; set; }
}

[TestMethod]
public void TestTextFormatter()
{
    var option = new JsonSerializeOption();
    option.Indent = true;
    var serializer = new JsonSerializer(option);

    var obj = new JsonData
        {
            Age = 12,
            Name = "fireasy",
            Birthday = DateTime.Today,
            WorkRecords = new List<WorkRecord> 
            { 
                new WorkRecord { Company = "zeek", StartDate = DateTime.Now } 
            }
        };

    var json = serializer.Serialize(obj);
        
    Console.WriteLine(json);
}

  输出的 Json 文本如下所示:

{
  "Name": "fireasy",
  "Age": "12.00",
  "Birthday": "2020年9月20日",
  "WorkRecords": [
    {
      "Company": "zeek",
      "StartDate", "2020-09-20 20:23:22"
    }
  ]
}

8、构造函数

  如果类定义了有参数的构造函数,Fireasy 也是支持反序列化的,如果参数名称与 json 里的属性名称不一致的话(忽略大小写),需要使用 TextSerializeParameterBindAttribute 特性进行标记。如下所示:

public class Person
{
    public Person(string name, [TextSerializeParameterBind("sr")]DateTime birthday)
    {
        Name = name;
        Birthday = birthday;
    }
        
    public string Name { get; set; }
    
    public DateTime? Birthday { get; set; }
        
    public int Age { get; set; }
}

[TestMethod]
public void TestDeserializeByConstructor()
{
    var json = "{ \"name\": \"fireasy\", \"sr\": \"2020-09-20\", \"age\": 12 }";

    var serializer = new JsonSerializer();
    var obj = serializer.Deserialize<Person>(json);
        
    Assert.AreEqual("fireasy", obj.Name);
    Assert.AreEqual(12, obj.Age);
}

9、实现 ITextSerializable

  可以使类实现 ITextSerializable 接口,在类里处理序列化和反序列化。这种方式侵入性和耦合性太强,不推荐使用。如下所示:

public class Person : ITextSerializable
{
    public string Name { get; set; }
    
    public DateTime? Birthday { get; set; }
        
    public int Age { get; set; }
        
    string ITextSerializable.Serialize(ITextSerializer serializer)
    {
        return string.Format("{ name: "{0}", birthday: "{1}", age: {2} }", Name, Birthday, Age);
    }
        
    void ITextSerializable.Deserialize(ITextSerializer serializer, string text)
    {
    }
}

[TestMethod]
public void TestTextSerializable()
{
    var serializer = new JsonSerializer();
        
    var obj = new Person { Name = "fireasy", Age = 12 };
        
    var json = serializer.Serialize(obj);
}

  输出的 Json 文本如下所示:

{ name: "fireasy", birthday: "null", age: 12 }

10、自定义 IContractResolver

  JsonSerializeOption 类有一个 ContractResolver 属性,是一个 IContractResolver 接口,它有以下两个方法:

  • GetProperties 方法

  返回指定类型的序列化属性元数据 SerializerPropertyMetadata。这个类包含了读值与赋值的委托函数、格式化、转换器等等。

  • ResolvePropertyName 方法

  用于识别 json 中的属性名称。


  IContractResolver 接口的默认实现是 DefaultContractResolver 类。


11、关于 ILazyManager

  Fireasy 的序列化考虑到了一种特殊的情况,即延迟加载的属性。这一类属性在对象初始化时,有可能不会被赋值,只有在访问该属性时才会被赋值(比如实体类型)。这在序列化时会造成不必要的查询和转换。再者这种属性存在非常大的隐患,试想如果两个类里的属性是相互关联的,那么将可能造成不断的递归调用,最终导致应用崩溃。

  Fireasy 引入了 ILazyManager 接口(可参考 ILazyManager),在序列化时,会使用 IsValueCreated 方法检测属性是否已经赋值,如果否则忽略该属性的处理,从而避免了延迟加载的发生。


12、反序列化处理器

  如果类型实现了 IDeserializeProcessor 接口,则可以处理属性的赋值,而不是使用 SerializerPropertyMetadata 中的赋值委托。