Json 序列化
Json (JavaScript Object Notation) 是一种轻量级的数据交换格式,由于它易于阅读和编写,同时也易于机器解析和生成,因此正成为一种被广泛应用的数据格式。 目前比较流行的Json序列化类库是 Newtonsoft.Json,Fireasy 基本上能够替代它。
Fireasy 的 Json 序列化支持的数据范围有:DataSet
、DataTable
、DataRow
、IEnumerable
、IDictionary
以及 String
、DateTime
等简单类型、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
类里会使用到 JsonWriter
和 JsonReader
对象,它们的方法见下一小节。
转换器除了在 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 方法: 读取一个任意类型的值,这些值可能是
int
、string
、bool
或DateTime
类型。ReadAs* 序列方法: 读取指定类型的值,如 ReadAsInt32、ReadAsString 等等。
ReadKey 方法: 读取一个属性名。
IsDelimiter 方法: 判断字符是否为界定符。界定符有
{
、}
、[
、]
、:
和 空格。IsNextCharacter 方法: 判断下一个字符是否与指定的字符相同。
IsWhiteSpace 方法: 判断指定的字符是否为空格。
IsNull 方法: 判断是否为 null。
AssertAndConsume 方法: 判断下一个字符是否与指定的字符相同,如果不相同则抛出异常。
AssertNextIsDelimiterOrSeparator 方法: 判断下一个字符是否是指定结束的字符,如果不相同则抛出异常。
5、动态类型与匿名类型
Fireasy 反序列化动态类型的机制与 Newtonsoft.Json 是不一样的,Fireasy 反序列化的对象是 dynamic
而不是 JToken
、JValue
等等,因此在访问该对象时要方便得多。如下所示:
[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
特性为 decimal
和 DateTime
类型的属性指定输出格式,如下所示:
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
中的赋值委托。