.NET?中的序列化和反序列化問(wèn)題示例詳解
.NET 中的序列化和反序列化
在 ASP.NET Core 應(yīng)用中,框架會(huì)屏蔽了很多實(shí)現(xiàn)序列化和反序列化的細(xì)節(jié),我們只需要定義參數(shù)模型,ASP.NET Core 會(huì)自動(dòng)將 http 請(qǐng)求的 Body 反序列化為模型對(duì)象。但是日常開(kāi)發(fā)中我們會(huì)對(duì)序列化和反序列化做許多定制配置,比如忽略值為 null 的字段、時(shí)間格式處理、忽略大小寫(xiě)、字段類型轉(zhuǎn)換等各種情況。因此筆者單獨(dú)使用一章講解序列化框架的使用以及如何進(jìn)行定制,深入了解 .NET 中序列化和反序列化機(jī)制。
System.Text.Json 是 .NET 框架自帶的序列化框架,簡(jiǎn)單易用并且性能也很出色,使用 System.Text.Json 反序列化字符串為對(duì)象是很簡(jiǎn)單的,示例如下:
// 自定義序列化配置
static JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
public static void Main()
{
const string json =
"""
{
"Name": "工良"
}
""";
var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
}
public class Model
{
public string Name { get; set; }
}JsonSerializerOptions 的屬性定義了如何序列化和反序列化,其常用屬性如下:
| 屬性 | 類型 | 說(shuō)明 |
|---|---|---|
| AllowTrailingCommas | bool | 忽略 JSON 中多余的逗號(hào) |
| Converters | IList<JsonConverter> | 轉(zhuǎn)換器列表 |
| DefaultBufferSize | int | 默認(rèn)緩沖區(qū)大小 |
| DefaultIgnoreCondition | JsonIgnoreCondition | 當(dāng)字段/屬性的值為默認(rèn)值時(shí),是否忽略 |
| DictionaryKeyPolicy | JsonNamingPolicy | 字典 Key 重命名規(guī)則,如首字母生成小寫(xiě) |
| IgnoreNullValues | bool | 忽略 JSON 中值為 null 的字段/屬性 |
| IgnoreReadOnlyFields | bool | 忽略只讀字段 |
| IgnoreReadOnlyProperties | bool | 忽略只讀屬性 |
| IncludeFields | bool | 是否處理字段,默認(rèn)只處理屬性 |
| MaxDepth | int | 最大嵌套深度,默認(rèn)最大深度為 64 |
| NumberHandling | JsonNumberHandling | 如何處理數(shù)字類型 |
| PropertyNameCaseInsensitive | bool | 忽略大小寫(xiě) |
| PropertyNamingPolicy | JsonNamingPolicy | 重命名規(guī)則,如首字母生成小寫(xiě) |
| ReadCommentHandling | JsonCommentHandling | 處理注釋 |
| WriteIndented | bool | 序列化時(shí)格式化 JSON,如換行、空格、縮進(jìn) |
接下來(lái)筆者將會(huì)列舉一些常用的定制場(chǎng)景和編碼方法,為了避免混肴,在本章中所指的 “字段” 或 “屬性”,等同于類型的“字段和屬性”。
編寫(xiě)類型轉(zhuǎn)換器
類型轉(zhuǎn)換器的作用是當(dāng) json 對(duì)象字段和模型類字段類型不一致時(shí),可以自動(dòng)轉(zhuǎn)換對(duì)應(yīng)的類型,下面筆者介紹常用的幾種類型轉(zhuǎn)換器。
枚舉轉(zhuǎn)換器
.NET 是如何序列化枚舉
編寫(xiě) WebAPI 的模型類時(shí)常常會(huì)用到枚舉,枚舉類型默認(rèn)會(huì)以數(shù)值的形式輸出到 json 中。
C# 代碼示例如下:
// 枚舉
public enum NetworkType
{
Unknown = 0,
IPV4 = 1,
IPV6 = 2
}
// 類型
public class Model
{
public string Name { get; set; }
public NetworkType Netwotk1 { get; set; }
public NetworkType? Netwotk2 { get; set; }
}
var model = new Model
{
Name = "工良",
Netwotk1 = NetworkType.IPV4,
Netwotk2 = NetworkType.IPV6
}當(dāng)我們序列化對(duì)象時(shí),會(huì)得到這樣的結(jié)果:
{
"Name": "工良",
"Netwotk1": 1,
"Netwotk2": 2
}
但是這樣會(huì)在閱讀上帶來(lái)難題,數(shù)字記憶比較困難,并且后期需要擴(kuò)展枚舉字段時(shí),可能會(huì)導(dǎo)致對(duì)應(yīng)數(shù)值的變化,那么已經(jīng)對(duì)接的代碼都需要修改,如果枚舉涉及的范圍比較廣,那么要做出修改就會(huì)變得十分困難。
比如說(shuō)突然出現(xiàn)了一個(gè) IPV5,那么我們除了改代碼,可能還要修改以及對(duì)接的其它應(yīng)用。
public enum NetworkType
{
Unknown = 0,
IPV4 = 1,
IPV5 = 2,
IPV6 = 3
}
因此,我們需要一種方法,能夠讓枚舉序列化后使用對(duì)應(yīng)的名稱表示,以及能夠使用這個(gè)字符串轉(zhuǎn)化為對(duì)應(yīng)的枚舉類型,后期需要擴(kuò)展或中間插入時(shí),對(duì)以前的代碼和數(shù)據(jù)庫(kù)完全沒(méi)有影響。
比如反序列化時(shí),得到的是這樣的 json:
"Netwotk1": "IPV4" "Netwotk2": "IPV6"
即使后來(lái)中間插入一個(gè) IPV5,生成新的字符串即可,完全不需要重新排序枚舉值。
"Netwotk1": "IPV4" "Netwotk2": "IPV6" "Netwotk3": "IPV5"
在 C# 模型類中使用枚舉而 json 中使用字符串,要實(shí)現(xiàn)這種形式的枚舉轉(zhuǎn)換,有兩種方法。
- 在模型類的枚舉字段或?qū)傩陨戏胖靡粋€(gè)特性注解,序列化反序列化時(shí)從這個(gè)特性注解中獲取轉(zhuǎn)換器。
- 使用 JsonSerializerOptions 添加轉(zhuǎn)換器,在反序列化或序列化時(shí)傳遞自定義配置。
無(wú)論哪種方法,我們都需要實(shí)現(xiàn)一個(gè)轉(zhuǎn)換器,能夠?qū)⒛P皖愔械拿杜e使用對(duì)應(yīng)的名稱序列化到 json 中。在實(shí)現(xiàn)自定義轉(zhuǎn)換器示例之前,我們來(lái)了解相關(guān)的一些知識(shí)。
自定義轉(zhuǎn)換器需要繼承 JsonConverter 或 JsonConverter<T>,當(dāng)反序列化 json 的字段或序列化對(duì)象的字段屬性時(shí),框架會(huì)自動(dòng)調(diào)用轉(zhuǎn)換器。
以 JsonConverter<T> 為例,里面有好幾個(gè)抽象接口,我們一般只需要實(shí)現(xiàn)轉(zhuǎn)換器的兩個(gè)抽象接口即可:
// json 值 => 對(duì)象字段 public abstract T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options); // 對(duì)象字段 => json 值 public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
不過(guò)我們一定要注意 C# 中的可空類型,比如 NetworkType 和 NetworkType? 實(shí)際上是兩種類型,可空類型本質(zhì)是使用 Nullable<T> 包裝的類型。
Nullable<T> 的定義如下:
public struct Nullable<T> where T : struct
另外 Nullable<T> 實(shí)現(xiàn)了和 T 類型的隱式和顯式轉(zhuǎn)換重載,所以我們?cè)谑褂每煽疹愋蜁r(shí),可能不太容易感受出 Nullable<T> 和 T 區(qū)別,比如可以在使用可空類型 T? 時(shí),直接將 Nullable<T> 與 T 類型隱式和顯式轉(zhuǎn)換,如:
Nullable<int> value = 100
但是在使用反射時(shí),由于 T 和 T? 是兩種不同的類型,因此我們編寫(xiě)轉(zhuǎn)換器時(shí)必須留意到這種區(qū)別,否則會(huì)出現(xiàn)錯(cuò)誤。
實(shí)現(xiàn)枚舉轉(zhuǎn)換器
本節(jié)示例代碼在 Demo4.Console 中。
編寫(xiě)一個(gè)枚舉字符串轉(zhuǎn)換器代碼示例如下:
public class EnumStringConverter<TEnum> : JsonConverter<TEnum>
{
private readonly bool _isNullable;
public EnumStringConverter(bool isNullType)
{
_isNullable = isNullType;
}
// 判斷當(dāng)前類型是否可以使用該轉(zhuǎn)換器轉(zhuǎn)換
public override bool CanConvert(Type objectType) => EnumStringConverterFactory.IsEnum(objectType);
// 從 json 中讀取數(shù)據(jù)
// JSON => 值
// typeToConvert: 模型類屬性/字段的類型
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 讀取 json
var value = reader.GetString();
if (value == null)
{
if (_isNullable) return default;
throw new ArgumentNullException(nameof(value));
}
// 是否為可空類型
var sourceType = EnumStringConverterFactory.GetSourceType(typeof(TEnum));
if (Enum.TryParse(sourceType, value.ToString(), out var result))
{
return (TEnum)result!;
}
throw new InvalidOperationException($"{value} 值不在枚舉 {typeof(TEnum).Name} 范圍中");
}
// 值 => JSON
public override void Write(Utf8JsonWriter writer, TEnum? value, JsonSerializerOptions options)
{
if (value == null) writer.WriteNullValue();
else writer.WriteStringValue(Enum.GetName(value.GetType(), value));
}
}由于 Utf8JsonReader 日常出行的機(jī)會(huì)不多,因此讀者可能不太了解,在本章的末尾,筆者會(huì)簡(jiǎn)單介紹。
一般情況下,我們不會(huì)直接使用 EnumStringConverter ,為了能夠適應(yīng)所有枚舉類型,還需要編寫(xiě)一個(gè)枚舉轉(zhuǎn)換工廠,通過(guò)工廠模式判斷輸入類型之后,再創(chuàng)建對(duì)應(yīng)的轉(zhuǎn)換器。
public class EnumStringConverterFactory : JsonConverterFactory
{
// 獲取需要轉(zhuǎn)換的類型
public static bool IsEnum(Type objectType)
{
if (objectType.IsEnum) return true;
var sourceType = Nullable.GetUnderlyingType(objectType);
return sourceType is not null && sourceType.IsEnum;
}
// 如果類型是可空類型,則獲取原類型
public static Type GetSourceType(Type typeToConvert)
{
if (typeToConvert.IsEnum) return typeToConvert;
return Nullable.GetUnderlyingType(typeToConvert);
}
// 判斷該類型是否屬于枚舉
public override bool CanConvert(Type typeToConvert) => IsEnum(typeToConvert);
// 為該字段創(chuàng)建一個(gè)對(duì)應(yīng)的類型轉(zhuǎn)換器
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var sourceType = GetSourceType(typeToConvert);
var converter = typeof(EnumStringConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converter, new object[] { sourceType != typeToConvert });
}
}當(dāng) System.Text.Json 處理一個(gè)字段時(shí),會(huì)調(diào)用 EnumStringConverterFactory 的 CanConvert 方法,如果返回 true,則會(huì)調(diào)用 EnumStringConverterFactory 的 CreateConverter 方法創(chuàng)轉(zhuǎn)換器,最后調(diào)用轉(zhuǎn)換器處理字段,這樣一來(lái),我們可以通過(guò)泛型類 EnumStringConverter<TEnum> 處理各種枚舉。
然后定義特性注解,能夠?qū)⒛P皖惖膶傩宰侄谓壎ǖ揭粋€(gè)轉(zhuǎn)換器上。
[AttributeUsage(AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class EnumConverterAttribute : JsonConverterAttribute
{
public override JsonConverter CreateConverter(Type typeToConvert)
{
return new EnumStringConverterFactory();
}
}如何使用類型轉(zhuǎn)換器
使用自定義類型轉(zhuǎn)換器有三種方法。
方法一,在枚舉字段中使用自定義特性:
public class Model
{
public string Name { get; set; }
[EnumConverter]
public NetworkType Netwotk1 { get; set; }
[EnumConverter]
public NetworkType? Netwotk2 { get; set; }
}方法二,使用 JsonConverter 特性。
public class Model
{
public string Name { get; set; }
[JsonConverter(typeof(EnumConverter))]
public NetworkType Netwotk1 { get; set; }
[JsonConverter(typeof(EnumConverter))]
public NetworkType? Netwotk2 { get; set; }
}方法三,在配置中添加轉(zhuǎn)換器。
jsonSerializerOptions.Converters.Add(new EnumStringConverterFactory()); var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
在模型類中使用轉(zhuǎn)換器特性之后,我們可以通過(guò)字符串反序列化為枚舉類型:
const string json =
"""
{
"Name": "工良",
"Netwotk1": "IPV4",
"Netwotk2": "IPV6"
}
""";
var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
使用官方的轉(zhuǎn)換器
System.Text.Json 中已經(jīng)實(shí)現(xiàn)了很多轉(zhuǎn)換器,可以在官方源碼的 System/Text/Json/Serialization/Converters/Value 下找到所有自帶的轉(zhuǎn)換器,其中官方實(shí)現(xiàn)的枚舉字符串轉(zhuǎn)換器叫 JsonStringEnumConverter ,使用方法跟我們的自定義轉(zhuǎn)換器一致。
這里我們可以使用官方的 JsonStringEnumConverter 轉(zhuǎn)換器替代 EnumStringConverter<TEnum>:
public class Model
{
public string Name { get; set; }
public NetworkType Netwotk1 { get; set; }
public NetworkType? Netwotk2 { get; set; }
}
JsonSerializerOptions jsonSerializerOptions = new();
jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
const string json =
"""
{
"Name": "工良",
"Netwotk1": "IPV4",
"Netwotk2": "IPV6"
}
""";
var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
字符串和值類型轉(zhuǎn)換
很多情況下,會(huì)在模型類下使用數(shù)值類型,序列化到 json 時(shí)使用字符串。比如對(duì)應(yīng)浮點(diǎn)型的數(shù)值,為了保證其準(zhǔn)確性,我們會(huì)使用字符串形式保存到 json 中,這樣可以避免傳輸時(shí)對(duì)浮點(diǎn)型處理而丟失其準(zhǔn)確性。又比如前端處理超過(guò) 16 位數(shù)值時(shí),數(shù)字會(huì)丟失精確度,16位數(shù)字存儲(chǔ)毫秒格式的時(shí)間戳足夠了,很多時(shí)候我們會(huì)使用分布式 id,雪花算法有很多種,其生成的 id 往往會(huì)超過(guò) 16 位。
JS 中處理超過(guò) 16 位數(shù)字時(shí),會(huì)出現(xiàn)很精確度丟失的問(wèn)題:
console.log(11111111111111111); 輸出: 11111111111111112 console.log(111111111111111111); 輸出: 111111111111111100
有個(gè)最簡(jiǎn)單的方法是在 JsonSerializerOptions 中將所有數(shù)值字段轉(zhuǎn)換為字符串:
new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString
};但是這樣會(huì)導(dǎo)致所有值類型字段序列化為 json 時(shí)變成字符串,如果只需要處理幾個(gè)字段而不是處理所有字段,那就需要我們自己編寫(xiě)類型轉(zhuǎn)換器了。
要實(shí)現(xiàn)字符串轉(zhuǎn)數(shù)值,需要考慮很多種數(shù)值類型,如 byte、int、double、long 等,從值類型轉(zhuǎn)換為字符串是很簡(jiǎn)單的,但是要實(shí)現(xiàn)一個(gè)字符串轉(zhuǎn)任意類型值類型,那就很麻煩,這也是我們編寫(xiě)轉(zhuǎn)換器的重點(diǎn)。
編寫(xiě) json 字符串和模型類值類型轉(zhuǎn)換器的代碼示例如下:
public class StringNumberConverter<T> : JsonConverter<T>
{
private static readonly TypeCode typeCode = Type.GetTypeCode(typeof(T));
// 從 json 中讀取字符串,轉(zhuǎn)換為對(duì)應(yīng)的值類型
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.Number:
if (typeCode == TypeCode.Int32)
{
if (reader.TryGetInt32(out var value))
{
return Unsafe.As<int, T>(ref value);
}
}
if (typeCode == TypeCode.Int64)
{
if (reader.TryGetInt64(out var value))
{
return Unsafe.As<long, T>(ref value);
}
}
if (typeCode == TypeCode.Decimal)
{
if (reader.TryGetDecimal(out var value))
{
return Unsafe.As<decimal, T>(ref value);
}
}
if (typeCode == TypeCode.Double)
{
if (reader.TryGetDouble(out var value))
{
return Unsafe.As<double, T>(ref value);
}
}
if (typeCode == TypeCode.Single)
{
if (reader.TryGetSingle(out var value))
{
return Unsafe.As<float, T>(ref value);
}
}
if (typeCode == TypeCode.Byte)
{
if (reader.TryGetByte(out var value))
{
return Unsafe.As<byte, T>(ref value);
}
}
if (typeCode == TypeCode.SByte)
{
if (reader.TryGetSByte(out var value))
{
return Unsafe.As<sbyte, T>(ref value);
}
}
if (typeCode == TypeCode.Int16)
{
if (reader.TryGetInt16(out var value))
{
return Unsafe.As<short, T>(ref value);
}
}
if (typeCode == TypeCode.UInt16)
{
if (reader.TryGetUInt16(out var value))
{
return Unsafe.As<ushort, T>(ref value);
}
}
if (typeCode == TypeCode.UInt32)
{
if (reader.TryGetUInt32(out var value))
{
return Unsafe.As<uint, T>(ref value);
}
}
if (typeCode == TypeCode.UInt64)
{
if (reader.TryGetUInt64(out var value))
{
return Unsafe.As<ulong, T>(ref value);
}
}
break;
case JsonTokenType.String:
IConvertible str = reader.GetString() ?? "";
return (T)str.ToType(typeof(T), null);
}
throw new NotSupportedException($"無(wú)法將{reader.TokenType}轉(zhuǎn)換為{typeToConvert}");
}
// 將值類型轉(zhuǎn)換為 json 字符串
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
switch (typeCode)
{
case TypeCode.Int32:
writer.WriteNumberValue(Unsafe.As<T, int>(ref value));
break;
case TypeCode.UInt32:
writer.WriteNumberValue(Unsafe.As<T, uint>(ref value));
break;
case TypeCode.Decimal:
writer.WriteNumberValue(Unsafe.As<T, decimal>(ref value));
break;
case TypeCode.Double:
writer.WriteNumberValue(Unsafe.As<T, double>(ref value));
break;
case TypeCode.Single:
writer.WriteNumberValue(Unsafe.As<T, uint>(ref value));
break;
case TypeCode.UInt64:
writer.WriteNumberValue(Unsafe.As<T, ulong>(ref value));
break;
case TypeCode.Int64:
writer.WriteNumberValue(Unsafe.As<T, long>(ref value));
break;
case TypeCode.Int16:
writer.WriteNumberValue(Unsafe.As<T, short>(ref value));
break;
case TypeCode.UInt16:
writer.WriteNumberValue(Unsafe.As<T, ushort>(ref value));
break;
case TypeCode.Byte:
writer.WriteNumberValue(Unsafe.As<T, byte>(ref value));
break;
case TypeCode.SByte:
writer.WriteNumberValue(Unsafe.As<T, sbyte>(ref value));
break;
default:
throw new NotSupportedException($"不支持非數(shù)字類型{typeof(T)}");
}
}
}編寫(xiě)字符串轉(zhuǎn)換為各種類型的值類型,主要有一個(gè)難點(diǎn)泛型轉(zhuǎn)換,我們使用 reader.TryGetInt32() 讀取 int 值之后,明明知道泛型 T 是 int,但是我們卻不能直接返回 int ,我們必須要有一個(gè)手段可以將值轉(zhuǎn)換為泛型 T。如果使用反射,會(huì)帶來(lái)很大的性能消耗,還可能伴隨著裝箱拆箱,所以這里使用了 Unsafe.As ,其作用是將轉(zhuǎn)換類型的指針,使得相關(guān)的值類型可以轉(zhuǎn)換為泛型 T。
實(shí)現(xiàn)字符串和值類型轉(zhuǎn)換器之后,接著實(shí)現(xiàn)轉(zhuǎn)換工廠:
public class JsonStringToNumberConverter : JsonConverterFactory
{
public static JsonStringToNumberConverter Default { get; } = new JsonStringToNumberConverter();
public override bool CanConvert(Type typeToConvert)
{
var typeCode = Type.GetTypeCode(typeToConvert);
return typeCode == TypeCode.Int32 ||
typeCode == TypeCode.Decimal ||
typeCode == TypeCode.Double ||
typeCode == TypeCode.Single ||
typeCode == TypeCode.Int64 ||
typeCode == TypeCode.Int16 ||
typeCode == TypeCode.Byte ||
typeCode == TypeCode.UInt32 ||
typeCode == TypeCode.UInt64 ||
typeCode == TypeCode.UInt16 ||
typeCode == TypeCode.SByte;
}
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var type = typeof(StringNumberConverter<>).MakeGenericType(typeToConvert);
var converter = Activator.CreateInstance(type);
if (converter == null)
{
throw new InvalidOperationException($"無(wú)法創(chuàng)建 {type.Name} 類型的轉(zhuǎn)換器");
}
return (JsonConverter)converter;
}
}時(shí)間類型轉(zhuǎn)換器
json 中規(guī)定了標(biāo)準(zhǔn)的時(shí)間格式,部分常用時(shí)間格式如下:
YYYY-MM-DDTHH:mm:ss.sssZ YYYY-MM-DDTHH:mm:ss.sss+HH:mm YYYY-MM-DDTHH:mm:ss.sss-HH:mm
示例:
2023-08-15T20:20:00+08:00
但是在項(xiàng)目開(kāi)發(fā)中,我們很多使用需要使用定制的格式,如 2023-02-15 20:20:20 ,那么就需要自行編寫(xiě)轉(zhuǎn)換器,以便能夠正確序列化或反序列化時(shí)間字段。
在 C# 中有一個(gè)指定 DateTtime 如何解析字符串時(shí)間的接口,即 DateTime.ParseExact(String, String, IFormatProvider),為了能夠適應(yīng)各種字符串時(shí)間格式,我們可以利用該接口將字符串轉(zhuǎn)換為時(shí)間。
編寫(xiě) json 字符串時(shí)間與 DateTime 互轉(zhuǎn)的代碼示例如下:
public class CustomDateTimeConverter : JsonConverter<DateTime>
{
private readonly string _format;
// format 參數(shù)是時(shí)間的字符串格式
public CustomDateTimeConverter(string format)
{
_format = format;
}
public override void Write(Utf8JsonWriter writer, DateTime date, JsonSerializerOptions options)
{
writer.WriteStringValue(date.ToString(_format));
}
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString() ?? throw new FormatException("當(dāng)前字段格式錯(cuò)誤");
return DateTime.ParseExact(value, _format, null);
}
}轉(zhuǎn)換器中不需要判斷 json 字符串時(shí)間的各種,而是在使用時(shí)指定格式在構(gòu)造函數(shù)中注入。使用示例:
jsonSerializerOptions.Converters.Add(new CustomDateTimeConverter("yyyy/MM/dd HH:mm:ss"));
其實(shí),使用默認(rèn)的 json 時(shí)間格式是一個(gè)很好的習(xí)慣。據(jù)筆者經(jīng)驗(yàn),在項(xiàng)目中修改默認(rèn)的 json 時(shí)間格式,在后期項(xiàng)目開(kāi)發(fā)和對(duì)接中,很有可能出現(xiàn)序列化問(wèn)題。如果某些地方需要更高精細(xì)度,如需要毫秒、使用轉(zhuǎn)換為時(shí)間戳、第三方系統(tǒng)對(duì)接需要特殊格式等,可以在需要的模型類上使用特性標(biāo)記對(duì)應(yīng)的時(shí)間轉(zhuǎn)換器格式,最好不要全局修改 json 時(shí)間格式。
從底層處理 JSON
在本節(jié)中,筆者將會(huì)介紹如何使用 Utf8JsonReader 高性能地解析 json 文件,然后編寫(xiě)對(duì) Utf8JsonReader 的性能測(cè)試,通過(guò)相關(guān)的示例讓讀者掌握 Utf8JsonReader 的使用,以及如何對(duì)代碼進(jìn)行性能測(cè)試。
Utf8JsonReader
Utf8JsonReader 和 Utf8JsonWriter 是 C# 中讀取寫(xiě)入 json 的高性能 API,通過(guò) Utf8JsonReader 和 Utf8JsonWriter 我們可以逐步讀取 json 或?qū)懭?json。
Utf8JsonReader 使用比較廣泛,例如官方的 JsonConfigurationProvider 便是使用 Utf8JsonReader 逐步讀取 json 文件,生成 key/value 結(jié)構(gòu),而在后面的章節(jié)中,筆者也會(huì)介紹如何利用 Utf8JsonReader 實(shí)現(xiàn) i18n 多語(yǔ)言的配置。由于 Utf8JsonReader 的使用最廣泛,而 Utf8JsonWriter 并不常見(jiàn),所以筆者只介紹 Utf8JsonReader 的使用方法。
Utf8JsonReader 和 Utf8JsonWriter 都是結(jié)構(gòu)體,其定義如下:
public ref struct Utf8JsonReader public ref struct Utf8JsonWriter
由于其是 ref 結(jié)構(gòu)體,因此使用上有較多限制,例如不能在異步中使用,不能作為類型參數(shù)在數(shù)組、 List<>、字典等中使用,只能被放到 ref struct 類型中當(dāng)作字段或?qū)傩裕蛟诤瘮?shù)參數(shù)中使用。使用 Utf8JsonReader 讀取 json 時(shí),開(kāi)發(fā)者需要自行處理閉合括號(hào) {}、[] 等,也需要自行判斷處理 json 類型,因此讀取過(guò)程也稍為復(fù)雜 。
下面,筆者來(lái)設(shè)定一個(gè)場(chǎng)景,就是使用 Utf8JsonReader 來(lái)實(shí)現(xiàn)讀取 json 文件,將讀取到的字段全都存到字典中,如果有多層結(jié)構(gòu),則使用 : 拼接層級(jí),生成 IConfiguration 中的能夠直接讀取的 key/value 格式。
比如:
// json
{
"A": {
"B": "test"
}
}
// C#
new Dictionary<string, string>()
{
{"A:B","test" }
};新建一個(gè)靜態(tài)類 ReadJsonHelper,在這個(gè)類型中編寫(xiě)解析 json 的代碼。
public static class ReadJsonHelper
{
}
首先是讀取字段值的代碼,當(dāng)從 json 讀取字段時(shí),如果字段不是對(duì)象或數(shù)組類型,則直接讀取其值即可。
// 讀取字段值
private static object? ReadObject(ref Utf8JsonReader reader)
{
switch (reader.TokenType)
{
case JsonTokenType.Null or JsonTokenType.None:
return null;
case JsonTokenType.False:
return reader.GetBoolean();
case JsonTokenType.True:
return reader.GetBoolean();
case JsonTokenType.Number:
return reader.GetDouble();
case JsonTokenType.String:
return reader.GetString() ?? "";
default: return null;
}
}
讀取 json 字段時(shí),我們會(huì)碰到復(fù)雜的嵌套結(jié)構(gòu),因此需要判斷當(dāng)前讀取的是對(duì)象還是數(shù)組,而且兩者可以相互嵌套,這就增加了我們的解析難度。
比如:
{
... ...
}
[... ...]
[{...}, {...} ...]第一步是判斷一個(gè) json 的根結(jié)構(gòu)是 {} 還是 [],然后逐步解析。
// 解析 json 對(duì)象
private static void BuildJsonField(ref Utf8JsonReader reader,
Dictionary<string, object> map,
string? baseKey)
{
while (reader.Read())
{
// 頂級(jí)數(shù)組 "[123,123]"
if (reader.TokenType is JsonTokenType.StartArray)
{
ParseArray(ref reader, map, baseKey);
}
// 碰到 } 符號(hào)
else if (reader.TokenType is JsonTokenType.EndObject) break;
// 碰到字段
else if (reader.TokenType is JsonTokenType.PropertyName)
{
var key = reader.GetString()!;
var newkey = baseKey is null ? key : $"{baseKey}:{key}";
// 判斷字段是否為對(duì)象
reader.Read();
if (reader.TokenType is JsonTokenType.StartArray)
{
ParseArray(ref reader, map, newkey);
}
else if (reader.TokenType is JsonTokenType.StartObject)
{
BuildJsonField(ref reader, map, newkey);
}
else
{
map[newkey] = ReadObject(ref reader);
}
}
}
}json 數(shù)組有很多種情況,json 數(shù)組的元素可以是任意類型,因此處理起來(lái)稍微麻煩,所以針對(duì)數(shù)組類型,我們還應(yīng)該支持解析元素,使用序號(hào)來(lái)訪問(wèn)對(duì)應(yīng)位置的元素。
解析數(shù)組:
// 解析數(shù)組
private static void ParseArray(ref Utf8JsonReader reader, Dictionary<string, object> map, string? baseKey)
{
int i = 0;
while (reader.Read())
{
if (reader.TokenType is JsonTokenType.EndArray) break;
var newkey = baseKey is null ? $"[{i}]" : $"{baseKey}[{i}]";
i++;
switch (reader.TokenType)
{
// [...,null,...]
case JsonTokenType.Null:
map[newkey] = null;
break;
// [...,123.666,...]
case JsonTokenType.Number:
map[newkey] = reader.GetDouble();
break;
// [...,"123",...]
case JsonTokenType.String:
map[newkey] = reader.GetString();
break;
// [...,true,...]
case JsonTokenType.True:
map[newkey] = reader.GetBoolean();
break;
case JsonTokenType.False:
map[newkey] = reader.GetBoolean();
break;
// [...,{...},...]
case JsonTokenType.StartObject:
BuildJsonField(ref reader, map, newkey);
break;
// [...,[],...]
case JsonTokenType.StartArray:
ParseArray(ref reader, map, newkey);
break;
default:
map[newkey] = JsonValueKind.Null;
break;
}
}
}最后,我們編寫(xiě)一個(gè)解析 json 的入口,通過(guò)用戶傳遞的 json 文件,解析出字典。
public static Dictionary<string, object> Read(ReadOnlySequence<byte> sequence,
JsonReaderOptions jsonReaderOptions)
{
var reader = new Utf8JsonReader(sequence, jsonReaderOptions);
var map = new Dictionary<string, object>();
BuildJsonField(ref reader, map, null);
return map;
}
JsonReaderOptions 用于配置 Utf8JsonReader 讀取策略,其主要屬性如下:
| 屬性 | 說(shuō)明 | |
|---|---|---|
| AllowTrailingCommas | bool | 是否允許(和忽略)對(duì)象或數(shù)組成員末尾多余的逗號(hào) |
| CommentHandling | JsonCommentHandling | 如何處理 JSON 注釋 |
| MaxDepth | int | 最大嵌套深度,默認(rèn)最大 64 層 |
讀取文件生成字典示例:
// 注意,不能直接 File.ReadAllBytes() 讀取文件,因?yàn)槲募?bom 頭
var text = Encoding.UTF8.GetBytes(File.ReadAllText("read.json"));
var dic = ReadJsonHelper.Read(new ReadOnlySequence<byte>(text), new JsonReaderOptions { AllowTrailingCommas = true });
在 Demo4.Console 示例項(xiàng)目中,有一個(gè) read.json 文件,其內(nèi)容較為復(fù)雜,可以使用這個(gè) json 驗(yàn)證代碼。

另外我們可以利用 Utf8JsonReader ,結(jié)合第三章中的自定義配置教程,將 json 文件解析到 IConfiguration 中。
var config = new ConfigurationBuilder() .AddInMemoryCollection(dic.ToDictionary(x => x.Key, x => x.Value.ToString())) .Build();
Utf8JsonReader 和 JsonNode 解析 JSON 性能測(cè)試
JsonNode 也是我們讀取 json 常用的方法之一,在本節(jié)中,筆者會(huì)介紹如何使用 BenchmarkDotNet 編寫(xiě)性能測(cè)試,對(duì)比 Utf8JsonReader 和 JsonNode 讀取 json 的性能。
在 Demo4.Benchmark 示例項(xiàng)目中,有三個(gè)存儲(chǔ)了大量對(duì)象數(shù)組的 json 文件,這些文件使用工具批量生成,我們將會(huì)使用這三個(gè) json 進(jìn)行性能測(cè)試。

對(duì)象格式:
{
"a_tttttttttttt": 1001,
"b_tttttttttttt": "邱平",
"c_tttttttttttt": "Nancy Lee",
"d_tttttttttttt": "buqdu",
"e_tttttttttttt": 81.26,
"f_tttttttttttt": 60,
"g_tttttttttttt": "1990-04-18 10:52:59",
"h_tttttttttttt": "35812178",
"i_tttttttttttt": "18935330000",
"j_tttttttttttt": "w.nsliozye@mbwrxiyf.ug",
"k_tttttttttttt": "浙江省 金華市 蘭溪市"
}首先安裝 BenchmarkDotNet 框架,然后創(chuàng)建一個(gè)性能測(cè)試入口加載 json 文件。
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.NativeAot80)]
[MemoryDiagnoser]
[ThreadingDiagnoser]
[MarkdownExporter, AsciiDocExporter, HtmlExporter, CsvExporter, RPlotExporter]
public class ParseJson
{
private ReadOnlySequence<byte> sequence;
[Params("100.json", "1000.json", "10000.json")]
public string FileName;
[GlobalSetup]
public async Task Setup()
{
var text = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, $"json/{FileName}"));
var bytes = Encoding.UTF8.GetBytes(text);
sequence = new ReadOnlySequence<byte>(bytes);
}
}在 ParseJson 中添加相關(guān)的方法,使用 Utf8JsonReader 解析 json :
[Benchmark]
public void Utf8JsonReader()
{
var reader = new Utf8JsonReader(sequence, new JsonReaderOptions());
U8Read(ref reader);
}
private static void U8Read(ref Utf8JsonReader reader)
{
while (reader.Read())
{
if (reader.TokenType is JsonTokenType.StartArray)
{
U8ReadArray(ref reader);
}
else if (reader.TokenType is JsonTokenType.EndObject) break;
else if (reader.TokenType is JsonTokenType.PropertyName)
{
reader.Read();
if (reader.TokenType is JsonTokenType.StartArray)
{
// 進(jìn)入數(shù)組處理
U8ReadArray(ref reader);
}
else if (reader.TokenType is JsonTokenType.StartObject)
{
U8Read(ref reader);
}
else
{
}
}
}
}
private static void U8ReadArray(ref Utf8JsonReader reader)
{
while (reader.Read())
{
if (reader.TokenType is JsonTokenType.EndArray) break;
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
U8Read(ref reader);
break;
// [...,[],...]
case JsonTokenType.StartArray:
U8ReadArray(ref reader);
break;
}
}
}在 ParseJson 中增加 JsonNode 解析 json 的代碼:
[Benchmark]
public void JsonNode()
{
var reader = new Utf8JsonReader(sequence, new JsonReaderOptions());
var nodes = System.Text.Json.Nodes.JsonNode.Parse(ref reader, null);
if (nodes is JsonObject o)
{
JNRead(o);
}
else if (nodes is JsonArray a)
{
JNArray(a);
}
}
private static void JNRead(JsonObject obj)
{
foreach (var item in obj)
{
var v = item.Value;
if (v is JsonObject o)
{
JNRead(o);
}
else if (v is JsonArray a)
{
JNArray(a);
}
else if (v is JsonValue value)
{
var el = value.GetValue<JsonElement>();
JNValue(el);
}
}
}
private static void JNArray(JsonArray obj)
{
foreach (var v in obj)
{
if (v is JsonObject o)
{
JNRead(o);
}
else if (v is JsonArray a)
{
JNArray(a);
}
else if (v is JsonValue value)
{
var el = value.GetValue<JsonElement>();
JNValue(el);
}
}
}
private static void JNValue(JsonElement obj){}然后在 Main 方法中啟動(dòng)性能 Benchmark 框架進(jìn)行測(cè)試。
static void Main()
{
var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
Console.Read();
}
以 Release 模式編譯項(xiàng)目后,啟動(dòng)程序進(jìn)行性能測(cè)試。
筆者所用機(jī)器配置:
AMD Ryzen 5 5600G with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores
可以看到兩者的性能差異比較大,所以在需要高性能的場(chǎng)景下,我們使用 Utf8JsonReader 的性能會(huì)高一點(diǎn),還可以降低內(nèi)存的使用量。
| Method | Job | FileName | Mean | Gen0 | Gen1 | Gen2 | Allocated |
|---|---|---|---|---|---|---|---|
| Utf8JsonReader | .NET 8.0 | 100.json | 42.87 us | - | - | - | - |
| JsonNode | .NET 8.0 | 100.json | 237.57 us | 37.1094 | 24.4141 | - | 312624 B |
| Utf8JsonReader | NativeAOT 8.0 | 100.json | 49.81 us | - | - | - | - |
| JsonNode | NativeAOT 8.0 | 100.json | 301.11 us | 37.1094 | 24.4141 | - | 312624 B |
| Utf8JsonReader | .NET 8.0 | 1000.json | 427.07 us | - | - | - | - |
| JsonNode | .NET 8.0 | 1000.json | 2,699.76 us | 484.3750 | 460.9375 | 199.2188 | 3120511 B |
| Utf8JsonReader | NativeAOT 8.0 | 1000.json | 494.87 us | - | - | - | - |
| JsonNode | NativeAOT 8.0 | 1000.json | 3,652.08 us | 484.3750 | 464.8438 | 199.2188 | 3120513 B |
| Utf8JsonReader | .NET 8.0 | 10000.json | 4,306.30 us | - | - | - | 3 B |
| JsonNode | .NET 8.0 | 10000.json | 60,883.56 us | 4000.0000 | 3888.8889 | 1222.2222 | 31215842 B |
| Utf8JsonReader | NativeAOT 8.0 | 10000.json | 4,946.71 us | - | - | - | 3 B |
| JsonNode | NativeAOT 8.0 | 10000.json | 62,864.68 us | 4125.0000 | 4000.0000 | 1250.0000 | 31216863 B |
到此這篇關(guān)于.NET 中的序列化和反序列化的文章就介紹到這了,更多相關(guān).NET 序列化和反序列化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 徹底掌握.NET中序列化反序列化問(wèn)題及避坑指南
- .NET 白名單文件通過(guò)反序列化執(zhí)行系統(tǒng)命令的方法
- C#使用Json.Net對(duì)JSON與對(duì)象的序列化與反序列化
- .NET中XML序列化和反序列化常用類和屬性小結(jié)
- 淺談.Net中的序列化和反序列化
- C#使用Json.Net進(jìn)行序列化和反序列化及定制化
- .Net中的序列化和反序列化詳解
- .net的序列化與反序列化實(shí)例
- .net實(shí)現(xiàn)序列化與反序列化實(shí)例解析
- ASP.NET中JSON的序列化和反序列化使用說(shuō)明
- asp.net 序列化and反序列化演示
- asp.net xml序列化與反序列化
相關(guān)文章
ASP.NET:一段比較經(jīng)典的多線程學(xué)習(xí)代碼
ASP.NET:一段比較經(jīng)典的多線程學(xué)習(xí)代碼...2006-09-09
HTTP錯(cuò)誤500.19解決方法(定義了重復(fù)的節(jié)點(diǎn))
HTTP 錯(cuò)誤 500.19 - Internal Server Error 無(wú)法訪問(wèn)請(qǐng)求的頁(yè)面,因?yàn)樵擁?yè)的相關(guān)配置數(shù)據(jù)無(wú)效2013-06-06
.NET Core3.0創(chuàng)建Worker Services的實(shí)現(xiàn)
這篇文章主要介紹了.NET Core3.0創(chuàng)建Worker Services的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
asp.net自定義控件中注冊(cè)Javascript問(wèn)題解決方案
這篇文章主要介紹了asp.net自定義控件中注冊(cè)Javascript的問(wèn)題,需要的朋友可以參考下2014-05-05
.Net中的Junction Points(交接點(diǎn))操作
這篇文章介紹了.Net中的Junction Points(交接點(diǎn))操作,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
ASP.NET MVC用存儲(chǔ)過(guò)程批量添加修改數(shù)據(jù)操作
這篇文章主要介紹了ASP.NET MVC用存儲(chǔ)過(guò)程批量添加修改數(shù)據(jù)的方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02
Asp.net簡(jiǎn)單實(shí)現(xiàn)給圖片增加文字水印
這篇文章主要介紹了Asp.net簡(jiǎn)單實(shí)現(xiàn)給圖片增加文字水印,需要的朋友可以參考下2014-12-12
Asp.net導(dǎo)出Excel/Csv文本格式數(shù)據(jù)的方法
這篇文章主要介紹了Asp.net導(dǎo)出Excel/Csv文本格式數(shù)據(jù)的方法,比較實(shí)用,需要的朋友可以參考下2014-09-09
詳解如何創(chuàng)建一個(gè).NET Core工程
本文主要介紹了如何創(chuàng)建一個(gè).NET Core工程,文中根據(jù)實(shí)例編碼詳細(xì)介紹的十分詳盡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
VSCode輕松調(diào)試運(yùn)行.Net?8.0?Web?API項(xiàng)目的過(guò)程
這篇文章主要介紹了VSCode輕松調(diào)試運(yùn)行.Net?8.0?Web?API項(xiàng)目的相關(guān)資料,包括插件安裝、配置文件創(chuàng)建及運(yùn)行調(diào)試步驟,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2025-06-06

