Serialize property by input string using System.Text.Json
前言
最近工作上,需要開發功能類似OData Web API 中的$select選項,讓用戶端自行決定從伺服器回傳Json哪些物件屬性。
https://learn.microsoft.com/zh-tw/aspnet/web-api/overview/odata-support-in-aspnet-web-api/using-select-expand-and-value
Json.Net 可以參考以下文章:
Json.NET-動態決定屬性是否序列化 - 黑暗執行緒
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?
本文介紹的是System.Text.Json
的作法
內文
實務工作上我要輸出的Json內容有可能這樣 ↓ (注意products集合裡的物件屬性數量)
{
"status": 200,
"error_msg": "",
"products": [
{
"modelName": "CMT-SBT300Test",
"baseModelName": "CMT-SBT300WBTT",
"SerialNo": "5100045aaa",
"purchasedDate": "2000-01-01"
},
{
"modelName": "CMT-SBT30Test",
"baseModelName": "CMT-SBT300TTBBCC",
"SerialNo": "5101000bbb",
"purchasedDate": "2020-01-01"
}
]
}
products集合裡的物件屬性也有可能減少成這樣 ↓
{
"status": 200,
"error_msg": "",
"products": [
{
"modelName": "CMT-SBT300Test",
"purchasedDate": "2000-01-01"
},
{
"modelName": "CMT-SBT30Test",
"purchasedDate": "2020-01-01"
}
]
}
以下提供上述案例的實作方式,說明在程式註解裡
※ 2023-04-27 追記:[.NET] System.Text.Json 序列化&反序列化使用不同的屬性名稱 - 高級打字員
↑ 這裡有更完整功能的範例程式碼
※以下是.Net Framework 4.8 的 Console專案,.Net Core應該大同小異,不過貌似TypeInfoResolver
、DefaultJsonTypeInfoResolver
這兩個類別在.NET 7+ 才有
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;
namespace ConsoleApp_JsonConvertTest
{
/// <summary>
/// 最終要回傳被序列化的物件
/// </summary>
public class MyResponseContent
{
/// <summary>
/// http 回應代碼
/// </summary>
public int status { get; set; } = 0;
/// <summary>
/// 錯誤訊息
/// </summary>
public string error_msg { get; set; } = "";
/// <summary>
/// ★List集合裡的物件屬性,最終序列化之後有時候多有時候少,所以宣告為object較有彈性
/// </summary>
public List<object> products { get; set; } = new List<object>();
}//end class MyResponseContent
public class Product
{
public string modelName { get; set; }
public string baseModelName { get; set; }
[JsonPropertyName("SerialNo")]
public string MySerialNo { get; set; }
[JsonPropertyName("purchasedDate")]
public string buy_date { get; set; }
}//end class Product
/// <summary>
/// [.NET] System.Text.Json 動態決定屬性是否序列化 - 高級打字員
/// https://www.dotblogs.com.tw/shadow/2023/04/25/211222
/// </summary>
public class MyJsonModifier
{
/// <summary>
/// 要序列化的屬性名稱,集合內容值以[JsonPropertyName]名稱為主儲存,其次為原始屬性名稱
/// </summary>
public List<string> SerializeJsonPropertyNames { get; set; } = new List<string>();
/// <summary>
/// 序列化部份屬性
/// </summary>
/// <param name="jti"></param>
public void SerializePartialProperty(JsonTypeInfo jti)
{
foreach (JsonPropertyInfo prop in jti.Properties)
{
//↓若屬性有套用 [JsonPropertyName] Attribute者,則優先取得 JsonPropertyName Attribute 的名稱;若沒套用,則直接取得原始屬性名稱。
string jsonPropertyName = prop.Name;
//此屬性是否序列化
prop.ShouldSerialize = (object myObject, object propType) =>
{
bool result = true;//預設序列化目前的Property
if (this._serializePropertieNames.Count > 0)//有指定要序列化屬性的白名單
{ //有在白名單內的JsonPropertyName才要序列化
result = this.SerializeJsonPropertyNames.Contains(jsonPropertyName, StringComparer.OrdinalIgnoreCase);//字串比對忽略大小寫
}//end if
return result;
};
}//end foreach
}//end SerializePartialProperty()
}
public class Program
{
private static MyJsonModifier myJsonModifier = new MyJsonModifier();
/// <summary>
/// 序列化的設定
/// </summary>
public static readonly JsonSerializerOptions jso = new JsonSerializerOptions()
{
//排版整齊
WriteIndented = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
/* ★參考文章「自訂 JSON 合約」:
https://learn.microsoft.com/zh-tw/dotnet/standard/serialization/system-text-json/custom-contracts
*/
Modifiers = { myJsonModifier.SerializePartialProperty }
},
/*
* 解決中文亂碼
* 如何使用 自訂字元編碼 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符號字串也可正常顯示中文,但留意前端若直接輸出至網頁上會有資安漏洞
*/
//如果要回傳的屬性值當中就是會有奇怪的符號,例如:「<>+」。才用這個↓
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
static void Main(string[] args)
{
//回傳給用戶端的物件
var objResponseContent = new MyResponseContent()
{
status = Convert.ToInt32(System.Net.HttpStatusCode.OK),
error_msg = "",
};
//假裝從DB撈出所有資料行(屬性)的資料,塞什麼值不是重點
var dbListData = new List<object>();
dbListData.Add(new Product() { modelName = "CMT-SBT300Test", baseModelName = "CMT-SBT300WBTT",
MySerialNo = "5100045aaa", buy_date = "2000-01-01" });
dbListData.Add(new Product() { modelName = "CMT-SBT30Test", baseModelName = "CMT-SBT300TTBBCC",
MySerialNo = "5101000bbb", buy_date = "2020-01-01" });
//↓假裝用戶端指定要序列化的物件屬性才2個(以小寫逗號區隔)
//★有標記JsonPropertyName就填JsonPropertyName名稱
string productPropertyNames = "modelName,purchasedDate";
//↓亂輸入的字串不會報錯,因為程式找不到對應名稱的物件屬性可序列化就會略過
//string productPropertyNames = "modelName,purchasedDate,亂輸入word";
myJsonModifier.SerializeJsonPropertyNames = productPropertyNames.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries).ToList();
//★先序列化products集合,以減少集合裡物件屬性
string strList = JsonSerializer.Serialize(dbListData, jso);
//上述json字串再反序列化為 List<object> 型別,待會指派給 回傳結果物件(objResponseContent)
List<object> list2 = JsonSerializer.Deserialize<List<object>>(strList);
objResponseContent.products = list2;
//清除序列化指定屬性白名單,避免影響最終輸出結果
myJsonModifier.SerializeJsonPropertyNames.Clear();
//最終結果序列化
string strResult = JsonSerializer.Serialize(objResponseContent,jso);
Console.WriteLine(strResult);//輸出json字串
}//end Main()
}//end class Program
}//end namespace
↓ 執行結果
以上是參考 MSDN 文章改寫出來
「自訂 JSON 合約」:https://learn.microsoft.com/zh-tw/dotnet/standard/serialization/system-text-json/custom-contracts
另一實作方法為繼承 DefaultJsonTypeInfoResolver
類別,覆寫 GetTypeInfo()
方法,如下(執行結果一模一樣),挑自己喜歡的Coding Style來用就好
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.Text.Unicode;
using System.Data.SqlClient;
using System.Runtime.CompilerServices;
namespace ConsoleApp_JsonConvertTest
{
/// <summary>
/// 最終要回傳被序列化的物件
/// </summary>
public class MyResponseContent
{
/// <summary>
/// http 回應代碼
/// </summary>
public int status { get; set; } = 0;
/// <summary>
/// 錯誤訊息
/// </summary>
public string error_msg { get; set; } = "";
/// <summary>
/// ★List集合裡的物件屬性,最終序列化之後有時候多有時候少,所以宣告為object較有彈性
/// </summary>
public List<object> products { get; set; } = new List<object>();
}//end class MyResponseContent
public class Product
{
public string modelName { get; set; }
public string baseModelName { get; set; }
[JsonPropertyName("SerialNo")]
public string MySerialNo { get; set; }
[JsonPropertyName("purchasedDate")]
public string buy_date { get; set; }
}//end class Product
#region System.Text.Json版本↓
//★繼承 DefaultJsonTypeInfoResolver 類別,覆寫 GetTypeInfo() 方法
public class MyContractResolver : DefaultJsonTypeInfoResolver
{
/// <summary>
/// 要序列化的屬性名稱,集合內容值以[JsonPropertyName]名稱為主儲存,其次為原始屬性名稱
/// </summary>
public List<string> SerializeJsonPropertyNames { get; set; } = new List<string>();
public override JsonTypeInfo GetTypeInfo(Type t, JsonSerializerOptions o)
{
JsonTypeInfo jti = base.GetTypeInfo(t, o);
foreach (JsonPropertyInfo prop in jti.Properties)
{
//↓若屬性有套用 [JsonPropertyName] Attribute者,則優先取得 JsonPropertyName Attribute 的名稱;若沒套用,則直接取得原始屬性名稱。
string jsonPropertyName = prop.Name;
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
#endregion
public class Program
{
/// <summary>
/// 序列化的設定
/// </summary>
public static readonly JsonSerializerOptions jso = new JsonSerializerOptions()
{
//排版整齊
WriteIndented = true,
TypeInfoResolver = new MyContractResolver(),//★改成自己的 ContractResolver
/*
* 解決中文亂碼
* 如何使用 自訂字元編碼 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符號字串也可正常顯示中文,但留意前端若直接輸出至網頁上會有資安漏洞
*/
//如果要回傳的屬性值當中就是會有奇怪的符號,例如:「<>+」。才用這個↓
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
static void Main(string[] args)
{
//回傳給用戶端的物件
var objResponseContent = new MyResponseContent()
{
status = Convert.ToInt32(System.Net.HttpStatusCode.OK),
error_msg = "",
};
//假裝從DB撈出所有資料行(屬性)的資料,塞什麼值不是重點
var dbListData = new List<object>();
dbListData.Add(new Product() { modelName = "CMT-SBT300Test", baseModelName = "CMT-SBT300WBTT", MySerialNo = "5100045aaa", buy_date = "2000-01-01" });
dbListData.Add(new Product() { modelName = "CMT-SBT30Test", baseModelName = "CMT-SBT300TTBBCC", MySerialNo = "5101000bbb", buy_date = "2020-01-01" });
//假裝用戶端指定要序列化的物件屬性才2個↓
//★有標記JsonPropertyName就填JsonPropertyName名稱
string productPropertyNames = "modelName,purchasedDate";
//★把 TypeInfoResolver 挖出來轉型成自己的 ContractResolver
var myResolver = (MyContractResolver)jso.TypeInfoResolver;
myResolver.SerializeJsonPropertyNames= productPropertyNames.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries).ToList();
//★先序列化products集合,以減少集合裡物件屬性
string strList = JsonSerializer.Serialize(dbListData, jso);
//上述json字串再反序列化為 List<object> 型別,待會指派給 回傳結果物件(objResponseContent)
List<object> list2 = JsonSerializer.Deserialize<List<object>>(strList);
objResponseContent.products = list2;
//清除序列化指定屬性的白名單,避免影響最終輸出結果
myResolver.SerializeJsonPropertyNames.Clear();
//最終結果序列化
string strResult = JsonSerializer.Serialize(objResponseContent,jso);
Console.WriteLine(strResult);//輸出json字串
}//end Main()
}//end class Program
}//end namespace
補充:留意以下程式碼在執行過一次的序列化or反序列化之後會報錯,別妄想二次指派 DefaultJsonTypeInfoResolver
物件給TypeInfoResolver
所以程式設計上,我不會傳遞參數SerializePropertieNames
給new DefaultJsonTypeInfoResolver()
的建構式,
而是把SerializePropertieNames
做成公開的Property可以反覆存取修改值。
//↓會發生例外狀況:
//System.InvalidOperationException: This JsonSerializerOptions instance is read-only or has already been used in serialization or deserialization.
jso.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
參考文章
System.Text.Json與 比較 Newtonsoft.Json ,並移轉至System.Text.Json
※MSDN官方文章,主要看 DefaultContractResolver 要改寫為 DefaultJsonTypeInfoResolver 類別
System.Text.Json improvements in .NET 7
※給出大方向、繼承類別的靈感
How to Exclude Properties From JSON Serialization in C#
※其它解決方案