.NET 8 Preview 1 中 SystemTextJson 的改進
Intro
System.Text.Json
是從 .NET Core 3.0 開始的一個新的 JSON 處理庫,在之後的版本中一直在完善和改善性能,在 .NET 8 Preview 1 中完善一些支援,具體更新如下
Improvements
Unmapped Json Property Handling
在之前的版本中,如果 json 裡 property 是不希望的内容不會有任何處理,在新版本中增加了沒有 mapping 的 json property 處理,可以在找不到 mapping 的時候報錯,示例如下:
file record Person(int Id, string Name);
var personJsonWithoutId = JsonSerializer.Serialize(new { Id = 1, Name = "1234", Age = 10 });
try
{
var p = JsonSerializer.Deserialize<Person>(personJsonWithoutId);
Console.WriteLine(p?.ToString());
}
catch (Exception e)
{
Console.WriteLine(e);
}
不指定沒有 mapping 的 JSON property 的時候預設是允許的,以上就會正常輸出,不會走到 exception,輸出如下:
Person { Id = 1, Name = 1234 }
當我們指定了要報錯的時候就會抛異常
try
{
var p = JsonSerializer.Deserialize<Person>(personJsonWithoutId,
new JsonSerializerOptions() {
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow
});
Console.WriteLine(p?.ToString());
}
catch (Exception e)
{
Console.WriteLine(e);
}
輸出結果如下:
System.Text.Json.JsonException: The JSON property 'Net8Sample.<>FE3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855__Person' could not be mapped to any .NET member contained in type 'Age'.
at System.Text.Json.ThrowHelper.ThrowJsonException_UnmappedJsonProperty(Type type, String unmappedPropertyName)
at System.Text.Json.JsonSerializer.LookupProperty(Object obj, ReadOnlySpan`1 unescapedPropertyName, ReadStack& state, JsonSerializerOptions options, Boolean& useExtensionProperty, Boolean createExtensionProperty)
at System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Deserialize(Utf8JsonReader& reader, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo`1 jsonTypeInfo, able`1 actualByteCount)
at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo`1 jsonTypeInfo)
at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
at Net8Sample.JsonSample.MissingMemberHandlingTest()
除了指定
JsonSerializerOptions
我們也可以針對某一個類型添加
JsonUnmappedMemberHandling
标記,示例如下:
[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)]
file record Person2
{
public required int Id { get; init; }
public required string Name { get; init; }
public string? JobTitle { get; set; }
}
try
{
var p = JsonSerializer.Deserialize<Person2>(personJsonWithoutId);
Console.WriteLine(p?.ToString());
}
catch (Exception e)
{
Console.WriteLine(e);
}
輸出結果和前面的示例類似:
System.Text.Json.JsonException: The JSON property 'Net8Sample.<>FE3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855__Person2' could not be mapped to any .NET member contained in type 'Age'.
at System.Text.Json.ThrowHelper.ThrowJsonException_UnmappedJsonProperty(Type type, String unmappedPropertyName)
at System.Text.Json.JsonSerializer.LookupProperty(Object obj, ReadOnlySpan`1 unescapedPropertyName, ReadStack& state, JsonSerializerOptions options, Boolean& useExtensionProperty, Boolean createExtensionProperty)
at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Deserialize(Utf8JsonReader& reader, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo`1 jsonTypeInfo, able`1 actualByteCount)
at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo`1 jsonTypeInfo)
at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
at Net8Sample.JsonSample.MissingMemberHandlingTest()
在 System.Text.Json 中有個特殊的特性,我們可以使用
JsonExtensionData
來比對那些沒有 mapping 的 JSON property,那兩個一起使用會不會報錯呢,我們也來試一下
file record PersonWithExtensionData
{
public required int Id { get; init; }
public required string Name { get; init; }
[JsonExtensionData]
public Dictionary<string,object>? Extensions { get; set; }
}
try
{
var p = JsonSerializer.Deserialize<PersonWithExtensionData>(personJsonWithoutId,
new JsonSerializerOptions() { UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow });
Console.WriteLine(JsonSerializer.Serialize(p));
}
catch (Exception e)
{
Console.WriteLine(e);
}
輸出結果如下:
{"Id":1,"Name":"1234","Age":10}
是否和你猜測的一緻呢
Interface Hierarchy
在之前的版本中如果我們使用接口進行序列化的話,接口繼承的屬性是不會被序列化的,比如下面的代碼:
file interface IBase
{
int Base { get; set; }
}
file interface IDerived : IBase
{
int Derived { get; set; }
}
file class DerivedImplement : IDerived
{
public int Base { get; set; }
public int Derived { get; set; }
}
IDerived value = new DerivedImplement() { Base = 0, Derived =1 };
var serializedValue = JsonSerializer.Serialize(value);
Console.WriteLine(serializedValue);
在 .NET 7 中輸出結果如下:
.NET 7 interface serialize output
在 .NET 8 Preview 1 輸出結果如下:
.NET 8 Preview interface serialize output
SnakeCaseNaming && KebabCaseNaming
在 .NET 8 Preview 1 中新增了兩種屬性名稱序列化方式,SnakeCase 和 KebabCase,兩種方式分别有 大寫形式和小寫形式,使用的時候在
JsonSerializerOptions
中指定
PropertyNamingPolicy
即可,我們直接看下示例吧
private static void SnakeCaseNamingTest()
{
var p = new Person2()
{
Id = 1,
Name = "Alice",
JobTitle = "Engineer"
};
var snakeCaseLowerJson = JsonSerializer.Serialize(p, new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
Console.WriteLine(snakeCaseLowerJson);
var snakeCaseUpperJson = JsonSerializer.Serialize(p, new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseUpper
});
Console.WriteLine(snakeCaseUpperJson);
}
輸出結果如下:
SnakeCaseNaming output
再來看下 KebabCase 的示例:
private static void KebabCaseNamingTest()
{
var p = new Person2()
{
Id = 1,
Name = "Alice",
JobTitle = "Engineer"
};
var kebabCaseLowerJson = JsonSerializer.Serialize(p, new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower
});
Console.WriteLine(kebabCaseLowerJson);
var kebabCaseUpperJson = JsonSerializer.Serialize(p, new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper
});
Console.WriteLine(kebabCaseUpperJson);
}
輸出結果如下:
KebabCaseNaming output
JsonSerializerOptions-ReadOnly
JsonSerializerOptions
中增加了
IsReadOnly
和
MakeReadOnly
兩個方法,我們可以在為某個類型的序列化指定了某些序列化選項之後調用
MakeReadOnly
方法來保證序列化選項不會再被修改來保證序列化行為的一緻性,下面是一個示例:
private static void JsonSerializerOptionsReadOnlyTest()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
};
Console.WriteLine($"IsReadOnly: {options.IsReadOnly}");
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
Console.WriteLine("PropertyNamingPolicy updated");
options.MakeReadOnly();
Console.WriteLine($"IsReadOnly: {options.IsReadOnly}");
try
{
options.PropertyNamingPolicy = ;
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
輸出結果如下:
從上面的輸出可以看得出來,在我們調用
MakeReadOnly
方法之前
IsReadOnly
會是
false
,是可以修改 options 的配置的,在調用之後
IsReadOnly
就變成
true
了,再修改 options 的配置就會抛異常
More
細心的小夥伴可能會發現第一個示例
Unmapped Json Property Handling
部分示例的異常資訊是有點問題的,property 和 type 資訊的位置反了,這是一個 BUG 。。,目前 bug 已經修複了,preview 2 應該就沒這個問題了,修複 PR 可以參考:https://github.com/dotnet/runtime/pull/81718
References
- https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-1/#json-improvements
- https://github.com/dotnet/runtime/issues/37483
- https://github.com/dotnet/runtime/pull/79945
- https://github.com/dotnet/runtime/pull/78788
- https://github.com/dotnet/runtime/pull/69613
- https://github.com/dotnet/runtime/pull/74431
- https://github.com/dotnet/runtime/pull/81718
- https://github.com/WeihanLi/SamplesInPractice/blob/master/net8sample/Net8Sample/JsonSample.cs