Use different name for serializing and deserializing with System.Text.Json
前言
這兩篇文章遇到的問題我也踩到地雷了
Use different name for serializing and deserializing with Json.Net
System.Text.Json.Deserialize<TValue> 轉回的物件值都是 null - 亂馬客
問題描述
我有一個WebAPI回應的JSON內容如下
[
{
"Product1": "CMT-Test300AA",
"buy_date": "2015-10-27",
"Giveaway": "Y"
},
{
"Product1": "CMT-Test300BB",
"buy_date": "2020-05-02",
"Giveaway": "N"
}
]
但我要反序列化為物件的類別定義如下
public class MyResult
{
[JsonPropertyName("modelName")]
public string Product1 { get; set; } = "";
[JsonPropertyName("purchasedDate")]
public string buy_date { get; set; } = "";
public string Giveaway { get; set; } = "";
}//end class
↓ MSDN對於JsonPropertyNameAttribute
的說明
這導致我在呼叫方法System.Text.Json.JsonSerializer.Deserialize<T>(stringAPIResponse)
反序列化為物件時,
由於對應找不到JsonPropertyNameAttribute
的名稱modelName
及purchasedDate
,反序列化後的物件雖然有拿到實例,
但Product1
及buy_date
這兩個屬性卻是預設值的空字串。
而類別的Property會加上JsonPropertyNameAttribute
是因為我想要在序列化的時候,輸出和DB資料庫不同的資料行名稱給前端用戶,
所以JsonPropertyNameAttribute
也不太可能拿掉。
以下考慮過幾種解法,但總覺得都怪怪的…
解法一:新增定義另一個類別來接WebAPI反序列化的物件,Property都沒有加上JsonPropertyNameAttribute
public class MyResultDeserialize
{ public string Product1 { get; set; } = "";
public string buy_date { get; set; } = "";
public string Giveaway { get; set; } = "";
}//end class
↓ 反序列化時
string strAPIResponse = "";//API回應的json字串
var result = JsonSerializer.Deserialize<List<MyResultDeserialize>>(strAPIResponse);//改用類別 MyResultDeserialize 來接
↑ 但此解法缺點為需要維護兩個相似的類別
解法二:反序列化時,改用Newtonsoft.Json.JsonConvert.DeserializeObject()
因為Json.NET不認識JsonPropertyName("名稱")這種Attribute,它是給System.Text.Json用的,所以反序列化時物件每個屬性名稱便都對應得到API回傳JSON內容的物件屬性
↓ 例如
string strAPIResponse = "";//API回應的json字串
var result = Newtonsoft.Json.JsonConvert.DeserializeObject<List<MyResult>>(strAPIResponse);//改用 Json.NET 來反序列化
↑ 但此解法缺點為系統裡,有的使用System.Text.Json
、有的地方使用 Json.NET,處理不一致很零散鎖碎。
解法三:捨棄JsonPropertyNameAttribute
,改用LINQ語法解決問題
在序列化時,使用LINQ的.Select()方法做集合轉型並在匿名型別中給予不同的屬性名稱,
之後再對匿名型別的集合做序列化並輸出給前端用戶,前端便看不到DB原始資料行名稱(有達到客戶需求)。
在反序列化時,則直接使用毫無JsonPropertyNameAttribute
修飾屬性的類別MyResult
,如此便接得到API回傳的每個屬性。
↓ 範例如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.Json;//.Net Framework 要先從Nuget套件安裝
using System.Text.Encodings.Web;
using System.Text.Json.Serialization;
//.Net Framework 4.8 Console 專案
namespace ConsoleApp1_TestDemoJsonLINQ
{
public class MyResult //屬性別加任何的 JsonPropertyNameAttribute
{
public string Product1 { get; set; } = "";
public string buy_date { get; set; } = "";
public string Giveaway { get; set; } = "";
}//end class
public class Program
{
public static readonly JsonSerializerOptions jso = new JsonSerializerOptions()
{
WriteIndented = true,//(序列化時使用) 排版整齊
//(序列化時使用) 序列化後的屬性大小寫維持原樣,若序列對象的屬性有套用 JsonPropertyNameAttribute 者為 JsonPropertyNameAttribute 名稱優先。
PropertyNamingPolicy = null,
PropertyNameCaseInsensitive = true,//(反序列化時使用) 不區分大小寫
NumberHandling = JsonNumberHandling.AllowReadingFromString,//(反序列化時使用) 允許從「字串」讀入「數值」型別的屬性
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping//(序列化時使用) 中文不亂碼,直接輸出特殊符號,例如:「<>+」
};
static void Main(string[] args)
{
//假裝從DB抓出兩筆資料
List<MyResult> dbList = new List<MyResult>();
dbList.Add(new MyResult { Product1 = "CMT-Test300AA", buy_date = "2015-10-27", Giveaway = "Y" });
dbList.Add(new MyResult { Product1 = "CMT-Test300BB", buy_date = "2020-05-02", Giveaway = "N" });
//在序列化前
//使用LINQ語法做集合轉型,並在匿名型別中給予不同的屬性名稱
var resultList = dbList.Select(m=>new
{
modelName = m.Product1,
purchasedDate = m.buy_date,
Giveaway = m.Giveaway
});//End LINQ
//序列化集合,輸出給前端用戶看
Console.WriteLine(JsonSerializer.Serialize(resultList,jso));
//假裝從API接到json字串
string strJson = "";
//API回傳JSON內容裡的每個屬性名稱都對應得到物件的屬性名稱,接回來的物件也就都拿得到值了
List<MyResult> listResult = JsonSerializer.Deserialize<List<MyResult>>(strJson,jso);
}//end Main()
}//end class Program
}
↑ 但此解法缺點在每次序列化前,都要手寫LINQ語法,一行一行地刻出匿名型別的每個屬性,要是輸出的屬性很多的話,豈不是寫到天荒地老…
最終採用的解法:繼承DefaultJsonTypeInfoResolver
類別來改寫
綜合之前文章的需求:[.NET 7] System.Text.Json 動態決定屬性是否序列化 - 高級打字員
此解法的好處是定義類別寫一遍,之後Reuse就相當方便。
參考以下的範例,說明都在註解裡。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Encodings.Web;
using System.Net.Http;
using System.Text;
using System.Reflection;
/*
此 Console 專案使用.Net Framework 4.8
*/
namespace ConsoleApp_JsonConvertTest
{
public class MyJsonTypeInfoResolver : DefaultJsonTypeInfoResolver
{
/// <summary>
/// 要序列化的屬性名稱,集合內容值以[JsonPropertyName]名稱為主來儲存,其次為原始屬性名稱
/// </summary>
public List<string> SerializeJsonPropertyNames { get; set; } = new List<string>();
/// <summary>
/// 序列化or反序列化時,是否使用JsonPropertyNameAttribute的屬性名稱
/// </summary>
public bool UseJsonPropertyName { get; set; } = true;
/// <summary>
/// 呼叫 Serialize 或 Deserialize 方法之後,↓以下方法只會執行一次,之後二次呼叫Serialize 或 Deserialize 方法,不會再執行
/// </summary>
/// <param name="t"></param>
/// <param name="o"></param>
/// <returns></returns>
public override JsonTypeInfo GetTypeInfo(Type t, JsonSerializerOptions o)
{
JsonTypeInfo jti = base.GetTypeInfo(t, o);
foreach (JsonPropertyInfo prop in jti.Properties)
{
//原始屬性名稱(不管是否套用 [JsonPropertyName] )
string propUnderlyingName = (prop.AttributeProvider as MemberInfo).Name;
//↓若屬性有套用 [JsonPropertyName] Attribute者,則優先取得 JsonPropertyName Attribute 的名稱;若沒套用,則直接取得原始屬性名稱。
string jsonPropertyName = prop.Name;
if (this.UseJsonPropertyName == false)
{
//序列化or反序列化都使用原始屬性名稱
prop.Name = propUnderlyingName;
}
//此屬性是否序列化
prop.ShouldSerialize = (object myObject, object propType) =>
{
bool result = true;//預設序列化目前的Property屬性
if (this.SerializeJsonPropertyNames.Count > 0)//有指定要序列化屬性的白名單
{//有在白名單內的JsonPropertyName才要序列化
//字串比較時,忽略大小寫
result = this.SerializeJsonPropertyNames.Contains(jsonPropertyName, StringComparer.OrdinalIgnoreCase);
}//end if
return result;
};
}//end foreach
return jti;
}//end GetTypeInfo()
}//end class
#region Model
public class MyParam
{
/// <summary>
/// 會員Guid
/// </summary>
public string strGuid { get; set; } = "";
}//end class MyParam
public class MyResult
{
/*
利用 JsonPropertyNameAttribute 讓用戶端看到序列化後的的Json內容裡屬性名稱被 Rename 過(而不是DB原始資料行名稱,資安比較安全)
*/
//掛上JsonPropertyNameAttribute後,會覆寫 JsonSerializerOptions 裡的 PropertyNamingPolicy 設定
[JsonPropertyName("modelName")]
public string Product1 { get; set; } = "";
[JsonPropertyName("purchasedDate")]
public string buy_date { get; set; } = "";
public string GiVeAwaY { get; set; } = "";
}//end class
#endregion
public class Program
{
/// <summary>
/// 共用的 JsonSerializerOptions
/// </summary>
public static readonly JsonSerializerOptions jso = new JsonSerializerOptions()
{
WriteIndented = true,//(序列化時使用) 排版整齊
//MSDN PropertyNamingPolicy 的說明↓
//https://learn.microsoft.com/zh-tw/dotnet/api/system.text.json.jsonserializeroptions.propertynamingpolicy?view=net-7.0#system-text-json-jsonserializeroptions-propertynamingpolicy
//(序列化時使用) 序列化後的屬性大小寫維持原樣,若序列對象的屬性有套用 JsonPropertyNameAttribute 者為 JsonPropertyNameAttribute 名稱優先。
PropertyNamingPolicy = null,
PropertyNameCaseInsensitive = true,//(反序列化時使用) 不區分大小寫
NumberHandling = JsonNumberHandling.AllowReadingFromString,//(反序列化時使用) 允許從「字串」讀入「數值」型別的屬性
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping//(序列化時使用) 解決中文亂碼,直接輸出特殊符號,例如:「<>+」
//參考MSDN官網文章:「如何使用 自訂字元編碼 System.Text.Json」
//https://learn.microsoft.com/zh-tw/dotnet/standard/serialization/system-text-json/character-encoding
/*
1.JavaScriptEncoder.Create(UnicodeRanges.All),正常顯示中文,但會編碼不安全的符號字串
2.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,不會編碼XSS符號,也可正常顯示中文,但留意前端若直接輸出至網頁上會有資安漏洞。如果要回傳的屬性值當中就是會有奇怪的符號,例如:「<>+」。才用這個↑
*/
};
/// <summary>
/// WebAPI網址
/// </summary>
private static readonly string WebApiUrl = "https://localhost:44340/XXXController/XXXAction/";
/// <summary>
/// 建立共用的 HttpClient
/// 若是.NET Core,建議使用 HTTPClientFactory 來建立 HttpClient
/// </summary>
public static readonly HttpClient _client = new HttpClient();
public static T PostWebApi<T>(string url, object objPostBody, bool useJsonPropertyNameMapping = false)
{
// 指定 authorization header(若有需要的話)
//client.DefaultRequestHeaders.Add("authorization", "token {api token}");
//.NET 5+ 推薦使用 PostAsJsonAsync 方法,效能會較好
//todo 留意PostAsJsonAsync() 方法的 Encoding.UTF8, "application/json"的設定是否自動有效?並更新blog
//HttpResponseMessage response = client.PostAsJsonAsync(url, objPostBody).Result;
// 要 Post傳遞給 WebAPI 的內容
StringContent content = new StringContent(JsonSerializer.Serialize(objPostBody), Encoding.UTF8, "application/json");
// 發出 http post (json body) 並取得結果
HttpResponseMessage response = _client.PostAsync(url, content).Result;
// 回應結果字串
string strResponse = response.Content.ReadAsStringAsync().Result;
/* JsonSerializerOptions 若被用過一次 Serialize 或 Deserialize,就不可再更改 TypeInfoResolver 的參考
例如:jso.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); →會報錯
所以才會用 myJso = new JsonSerializerOptions(jso);
重新複製一份 Json 設定的參考
*/
var myJso = new JsonSerializerOptions(jso)
{
TypeInfoResolver = new MyJsonTypeInfoResolver() { UseJsonPropertyName = useJsonPropertyNameMapping };
};
var result = JsonSerializer.Deserialize<T>(strResponse, myJso);
if (result == null)
{//為了消除VS IDE的null警告...
result = Activator.CreateInstance<T>();
}
return result;
}
static void Main(string[] args)
{
#region 發出 WebAPI 傳遞會員guid
string g_user_id = "{Test-test-Test-test-Test}";
Console.WriteLine("↓呼叫API useJsonPropertyNameMapping: true");
Console.WriteLine("※若 JsonPropertyName 和 WebAPI 回傳的屬性不同,該屬性值會是預設值(空字串);若屬性未套用 JsonPropertyNameAttrubite,則比對原始屬性名稱來反序列化");
Console.WriteLine("");
var listResult = PostWebApi<List<MyResult>>(WebApiUrl,
new //要傳遞的JsonBody
{
strGuid = g_user_id
},
//具名參數好閱讀↓
useJsonPropertyNameMapping: true);
//顯示結果,假裝從DB只取出兩筆
foreach (var item in listResult.Take(2))
{
Console.WriteLine($"{nameof(item.Product1)}:{(item.Product1==""?"空字串":item.Product1)},{nameof(item.buy_date)}:{(item.buy_date==""?"空字串":item.buy_date)},{nameof(item.GiVeAwaY)}:{item.GiVeAwaY}");
}
Console.WriteLine("====================");
Console.WriteLine("↓呼叫API useJsonPropertyNameMapping: false");
Console.WriteLine("※ WebAPI 回傳的屬性名稱直接和物件原始屬性名稱進行比對來反序列化(不管類別屬性是否套用 JsonPropertyNameAttrubite )");
Console.WriteLine("");
listResult = PostWebApi<List<MyResult>>(WebApiUrl,
new //要傳遞的JsonBody
{
strGuid = g_user_id
},
//具名參數好閱讀↓
useJsonPropertyNameMapping: false);
//顯示結果,假裝從DB只取出兩筆
foreach (var item in listResult.Take(2))
{
Console.WriteLine($"{nameof(item.Product1)}:{(item.Product1==""?"空字串":item.Product1)},{nameof(item.buy_date)}:{(item.buy_date==""?"空字串":item.buy_date)},{nameof(item.GiVeAwaY)}:{item.GiVeAwaY}");
}
#endregion
Console.WriteLine("====================");
#region 序列化....
var obj = new MyResult()
{
Product1 = "我的Product1",
buy_date = "2000-01-01",
GiVeAwaY = "贈品(7-11商品卡)",
};
Console.WriteLine("↓序列化 UseJsonPropertyName = true,指定只要輸出的JsonPropertyName(modelName,purchasedDate)");
//複製序列化的設定
var myJso1 = new JsonSerializerOptions(jso)
{
TypeInfoResolver = new MyJsonTypeInfoResolver()
{
SerializeJsonPropertyNames = "modelName,purchasedDate".Split(new string[] { "," },StringSplitOptions.RemoveEmptyEntries).ToList(),
UseJsonPropertyName = true
}
};
string strJson = JsonSerializer.Serialize(obj, myJso1);
Console.WriteLine(strJson);
Console.WriteLine("====================");
Console.WriteLine("↓序列化 UseJsonPropertyName = false(輸出原始屬性名稱),全部屬性都要序列化");
//複製序列化的設定
var myJso2 = new JsonSerializerOptions(jso)
{
TypeInfoResolver = new MyJsonTypeInfoResolver()
{
UseJsonPropertyName = false
}
};
strJson = JsonSerializer.Serialize(obj, myJso2);
Console.WriteLine(strJson);//所有結果原始輸出
Console.WriteLine("====================");
Console.WriteLine("↓序列化 UseJsonPropertyName = false(輸出原始屬性名稱),指定只要輸出的JsonPropertyName(modelName,GiVeAwaY)");
//複製序列化的設定
var myJso3 = new JsonSerializerOptions(jso);
myJso3.TypeInfoResolver = new MyJsonTypeInfoResolver()
{
//只想要序列化的JsonPropertyName
SerializeJsonPropertyNames = "modelName,GiVeAwaY".Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries).ToList(),
//但序列化後卻想要輸出的是原始屬性名稱
UseJsonPropertyName = false
};
strJson = JsonSerializer.Serialize(obj, myJso3);
Console.WriteLine(strJson);
#endregion
}//end Main()
}//end class Program
}//end namespace
↓ 執行結果
參考文章
MSDN官方文章:
如何在 .NET 中序列化和還原序列化 (封送處理和 unmarshal) JSON
↑ System.Text.Json 基本用法
System.Text.Json JsonSerializerOptions 類別
如何使用 System.Text.Json 自訂屬性名稱與值
自訂 JSON 合約
網友部落格:
反序列化的設定參考文章:
「System.Text.Json 可使用 JsonSerializerDefaults.Web 處理常見的 JSON 格式 - Will 保哥」
ASP.NET Core – Configure JSON serializer options
C# – Deserialize a JSON array to a list
討論文:
使用 System.Text.Json 找出原始物件屬性名稱:
System.Text.Json: In .NET 7, how can I determine the JsonPropertyInfo created for a specific member, so I can customize the serialization of that member?
這我也還在擱置研究中:
How to globally set default options for System.Text.Json.JsonSerializer?
List<string>的.Contains() 函數裡的參數,想要字串比較時忽略大小寫,請使用:「StringComparer.OrdinalIgnoreCase」,文章說明↓
Case-Insensitive List Search