

使用 Json.NET 與 QuickType 搭配字串轉 Enum 的絕佳解決方案
source link: https://blog.miniasp.com/post/2021/06/13/QuickType-String-to-Enum-Converter-using-NewtonsoftJson
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我每次需要呼叫遠端 Web API 的時候,都會盡量使用強型別的方式建立模型類別,但我基本上都不自己寫 Code,而是透過工具從 JSON 直接轉成模型類別。雖然說方便歸方便,但難免還是會遇到惱人的地雷,今天這篇文章描述問題的份量會比解決方案來的多,請大家一步一步的看下去,就可以理解整個來龍去脈。
重現問題的步驟
以下皆使用 Visual Studio Code 完成專案開發,實作前請先安裝 .NET Core Extension Pack 擴充套件!
如果你的 VSCode 同時安裝了 Paste JSON as Code (Refresh) 與 Paste JSON as Code 擴充套件,請記得移除 Paste JSON as Code 這個已經不再維護的擴充套件。
-
建立 Console 應用程式
mkdir c1 cd c1 dotnet new globaljson --sdk-version 5.0.301 dotnet new console dotnet add package System.Net.Http dotnet add package Newtonsoft.Json code .
因為 QuickType 會用到 Newtonsoft.Json 套件(俗稱 Json.NET),所以我們先安裝起來。
-
建立 Model 類別
先連到 臺北市政府行政機關辦公日曆表 的 API 連結,並複製完整的 JSON 內容到剪貼簿。
回到 VSCode 建立
Holiday.cs
檔案,執行 Paste JSON as Type 命令,看到 Top-level type name? 請輸入Holiday
,接著 VSCode 就會全自動幫你產生模型類別!注意: 預設命名空間為
QuickType
-
嘗試使用 System.Net.Http.Json 提供的擴充方法取得強型別的結果
dotnet add package System.Net.Http.Json
完成
Program.cs
程式碼如下:using System.Net.Http; using System.Net.Http.Json; using QuickType; using System.Threading.Tasks; namespace c1 { class Program { static async Task Main(string[] args) { using var client = new HttpClient(); var result = await client.GetFromJsonAsync<Holiday>("https://data.taipei/api/v1/dataset/29d9771d-c0ee-40d4-8dfb-3866b0b7adaa?scope=resourceAquire&offset=958&limit=1000"); } } }
-
此時你透過
dotnet run
就會發現,你會得到一個序列化過程的例外狀況System.Text.Json.JsonException
,也是本文想要分享的主要問題Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to QuickType.HolidayCategory. Path: $.result.results[0].holidayCategory | LineNumber: 0 | BytePositionInLine: 169. at System.Text.Json.ThrowHelper.ThrowJsonException(String message) at System.Text.Json.Serialization.Converters.EnumConverter`1.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value) at System.Text.Json.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader) 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.Converters.IEnumerableDefaultConverter`2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value) at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value) at System.Text.Json.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader) 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.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader) 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.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state) at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan`1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase) at System.Text.Json.JsonSerializer.ReadAsync[TValue](Stream utf8Json, Type returnType, JsonSerializerOptions options, CancellationToken cancellationToken) at System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsyncCore[T](HttpContent content, Encoding sourceEncoding, JsonSerializerOptions options, CancellationToken cancellationToken) at System.Net.Http.Json.HttpClientJsonExtensions.GetFromJsonAsyncCore[T](Task`1 taskResponse, JsonSerializerOptions options, CancellationToken cancellationToken) at c1.Program.Main(String[] args) in G:\Projects\c1\Program.cs:line 14 at c1.Program.<Main>(String[] args)
自製 HttpClientJsonExtensions 擴充方法 (Newtonsoft.Json)
由於透過 QuickType 所產生的模型類別,全部都是以 Newtonsoft.Json 套件為主,而且與 System.Text.Json 不太相容,所以你透過 Paste JSON as Code (Refresh) 套件所產生的程式碼,如果遇到稍微複雜的 JSON 格式,很有可能就沒辦法讓 Serialize/Deserialize 正常運作!
這裡主要是 JsonConverter 的實作方式不同,所以你不能混合使用這兩個不同的套件!
這裡我用了一個 GetFromJsonAsync 方法,非常的好用,可惜並無法跟 Newtonsoft.Json 混合使用。
其實要為了 Newtonsoft.Json 實作另一份 HttpClientJsonExtensions
並不是特別困難,你可以到 System.Net.Http.Json 參考一下寫法,改成 Newtonsoft.Json 的版本即可。
以下就是我的一份簡易實作版本:
-
先移除
System.Net.Http.Json
套件參考dotnet remove package System.Net.Http.Json
-
移除引用 System.Net.Http.Json 命名空間
using System.Net.Http.Json;
-
加入
HttpClientJsonExtensions.cs
類別using System.Text; using System.Net.Http; using Newtonsoft.Json; namespace c1 { public static class HttpClientJsonExtensions { public static async System.Threading.Tasks.Task<HttpResponseMessage> PostAsJsonAsync<T>(this HttpClient client, string requestUrl, T theObj) { return await client.PostAsync(requestUrl, new StringContent(JsonConvert.SerializeObject(theObj), Encoding.UTF8, "application/json")); } public static async System.Threading.Tasks.Task<T> GetFromJsonAsync<T>(this HttpClient client, string requestUrl) { return JsonConvert.DeserializeObject<T>(await client.GetStringAsync(requestUrl)); } } }
-
現在,我們已經全部都改成用 Newtonsoft.Json 來轉換 JSON 資料了,我們再
dotnet run
測試一次,你會發現這次變得不太一樣了!但我們這次得到的依然是序列化過程的例外狀況Newtonsoft.Json.JsonSerializationException
:Unhandled exception. Newtonsoft.Json.JsonSerializationException: Error converting value "星期六、星期日" to type 'QuickType.HolidayCategory'. Path 'result.results[1].holidayCategory', line 1, position 241. ---> System.ArgumentException: Requested value '星期六、星期日' was not found. at Newtonsoft.Json.Utilities.EnumUtils.ParseEnum(Type enumType, NamingStrategy namingStrategy, String value, Boolean disallowNumber) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType) --- End of inner exception stack trace --- at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateList(IList list, JsonReader reader, JsonArrayContract contract, JsonProperty containerProperty, String id) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateList(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, Object existingValue, String id) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent) at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType) at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType) at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings) at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings) at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value) at c1.HttpClientJsonExtensions.GetFromJsonAsync[T](HttpClient client, String requestUrl) in G:\Projects\c1\HttpClientJsonExtensions.cs:line 16 at c1.Program.Main(String[] args) in G:\Projects\c1\Program.cs:line 13 at c1.Program.<Main>(String[] args)
使用 Newtonsoft.Json 的方式解決問題
我們先來看一下這個錯誤訊息:
Error converting value "星期六、星期日" to type 'QuickType.HolidayCategory'.
Path 'result.results[1].holidayCategory', line 1, position 241.
首先,這很明顯是 QuickType.HolidayCategory
這個型別無法進行轉換,其原始碼如下:
public enum HolidayCategory
{
放假之紀念日及節日,
星期六星期日,
特定節日,
補假,
補行上班日,
調整放假日
};
其次,則是我們的第 2 筆資料有問題 (result.results[1]
),我特別把 JSON 撈出來看一下:
{
"description": "",
"holidayCategory": "星期六、星期日",
"isHoliday": "是",
"date": "2021/1/2",
"_id": 960,
"name": ""
}
看來是因為 Newtonsoft.Json 預設的轉換器無法將 星期六、星期日
這個值,對應到 enum HolidayCategory
型別的 星期六星期日
項目。兩者之間確實不太一樣,這當中差了一個頓號(、
)!
還好 Json.NET 有內建一個 StringEnumConverter 字串到 Enum 的轉換器,你只要在列舉的項目加上一個 EnumMember
Attribute 就可以幫助我們輕鬆的解決問題,請把 enum HolidayCategory
修改成以下定義即可:
using System.Runtime.Serialization;
public enum HolidayCategory {
放假之紀念日及節日,
[EnumMember(Value = "星期六、星期日")]
星期六星期日,
特定節日,
補假,
補行上班日,
調整放假日
};
最終版的 Program.cs
內容如下:
using System.Net.Http;
using QuickType;
using System.Threading.Tasks;
using System.Net.Http.Json;
namespace c1
{
class Program
{
static async Task Main(string[] args)
{
using var client = new HttpClient();
var result = await client.GetFromJsonAsync<Holiday>("https://data.taipei/api/v1/dataset/29d9771d-c0ee-40d4-8dfb-3866b0b7adaa?scope=resourceAquire&offset=958&limit=1000");
foreach (var item in result.Result.Results)
{
System.Console.WriteLine(item.Date + " 假日: " + item.IsHoliday );
}
}
}
}
完整範例: https://github.com/doggy8088/NewtonsoftJsonStringEnumConverterDemo/commits/main
有沒有 System.Text.Json 的解法?
網路上我找不到,自己寫肯定是寫的出來的,花時間下去,沒有解決不了的問題。但我還是熱愛 Json.NET 的成熟穩重,所以短期內並沒有打算撰寫 System.Text.Json 的 StringEnumConverter
實作。
如果各位有興趣進一步研究的話,可以到這個網址找到大量的範例,看能不能如法炮製,做出好用的 StringEnumConverter
實作,如果有寫出來,請記得留言告訴我! 😄
Recommend
-
192
Try quicktype in your browser.
-
45
Whether you're using C#, Swift, TypeScript, Go, C++ or other languages, quicktype generates models and helper code for quickly and safely reading JSON in your apps. Customize online with advanced options, or download a command-line tool.
-
12
強調可提供絕佳真實使用體驗,Intel Evo 平台驗證輕薄筆電陸續登場Project Athena 規範和關鍵體驗指標、Evo 平台認證,將成為採購筆電的簡易參考指標。
-
7
PCIe 4.0 入門解決方案,Western Digital 推出 WD_Black SN750 SE NVMe SSD 延續 WD_Black SN750 名號,但是核心設計解決方案大不同。
-
10
使用 luit 完美解決 Windows Subsystem for Linux 顯示 Big5 字集的問題對於 WSL (Windows Subsystem for Linux) 一直無法處理 Big5 字元這件事,對我來說就像在一...
-
13
使用微軟及 Google Authenticator App-黑暗執行緒這年頭,多重要素驗證(Mutli-Factor Authentication, MFA)幾乎已成系統安全管控的基本要求,除了帳號密...
-
7
quicktype在线工具:将JSON自动转为各种编程语言 解道Jdon ...
-
5
大立光6.03亿元投资散热解決方案商三實科技,或意在苹果VR/MR_VR陀螺 ...
-
7
Crucial T700 PCIe Gen5 NVMe M.2 SSD 曝光,採用 Phison E26 設計解決方案
-
7
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK