來自 ASP.NET Core API 的 JSON 響應中缺少派生類型的屬性
我的 ASP.NET Core 3.1 API 控制器的 JSON 響應缺少屬性。當屬性使用派生類型時會發生這種情況;在派生類型中定義但不在基/介面中定義的任何屬性都不會序列化為 JSON。似乎在響應中缺乏對多態性的支持,就好像序列化是基於屬性的定義類型而不是其執行時類型一樣。如何更改此行為以確保所有公共屬性都包含在 JSON 響應中?
例子:
我的 .NET Core Web API 控制器返回此對象,該對象具有介面類型的屬性。
// controller returns this object public class Result { public IResultProperty ResultProperty { get; set; } // property uses an interface type } public interface IResultProperty { }這是一個派生類型,它定義了一個名為 的新公共屬性
Value。public class StringResultProperty : IResultProperty { public string Value { get; set; } }如果我像這樣從控制器返回派生類型:
return new MainResult { ResultProperty = new StringResultProperty { Value = "Hi there!" } };那麼實際的響應包括一個空對象(
Value屬性缺失):我希望得到的回應是:
{ "ResultProperty": { "Value": "Hi there!" } }
我最終創建了一個自定義
JsonConverter(System.Text.Json.Serialization 命名空間),它強制JsonSerializer序列化為對象的執行時類型。請參閱下面的解決方案部分。它很長,但效果很好,並且不需要我在 API 設計中犧牲物件導向的原則。一些背景知識: Microsoft 有一個
System.Text.Json序列化指南,其中有一個標題為“序列化派生類的屬性”的部分,其中包含與我的問題相關的良好資訊。特別是它解釋了為什麼派生類型的屬性沒有序列化:此行為旨在幫助防止意外暴露派生的執行時創建的類型中的數據。
如果這不是您關心的問題,那麼可以
JsonSerializer.Serialize通過顯式指定派生類型或指定來覆蓋呼叫中的行為object,例如:// by specifying the derived type jsonString = JsonSerializer.Serialize(objToSerialize, objToSerialize.GetType(), serializeOptions); // or specifying 'object' works too jsonString = JsonSerializer.Serialize<object>(objToSerialize, serializeOptions);要使用 ASP.NET Core 完成此操作,您需要掛鉤到序列化過程。我使用自定義 JsonConverter 執行此操作,該 JsonConverter 呼叫 JsonSerializer.Serialize 上面顯示的一種方式。我還實現了對反序列化的支持,雖然在原始問題中沒有明確要求,但無論如何幾乎總是需要。(奇怪的是,只支持序列化而不支持反序列化被證明是很棘手的。)
解決方案
我創建了一個基類,
DerivedTypeJsonConverter它包含所有的序列化和反序列化邏輯。對於您的每個基本類型,您都將為它創建一個對應的轉換器類,它派生自DerivedTypeJsonConverter. 這在下面的編號方向中進行了解釋。此解決方案遵循 Json.NET 的“類型名稱處理”約定,該約定引入了對 JSON 的多態性支持。它通過在派生類型的 JSON(例如:)中包含一個額外的**$type**
"$type":"StringResultProperty"屬性來工作,該屬性告訴轉換器對象的真實類型是什麼。(一個區別:在 Json.NET 中,$type 的值是一個完全限定的類型 + 程序集名稱,而我的 $type 是一個自定義字元串,它有助於防止命名空間/程序集/類名稱的變化。)API 呼叫者應該包括派生類型的 JSON 請求中的 $type 屬性。序列化邏輯通過確保對象的所有公共屬性都被序列化來解決我最初的問題,並且為了保持一致性,$type 屬性也被序列化。方向:
**1)**將下面的 DerivedTypeJsonConverter 類複製到您的項目中。
using System; using System.Collections.Generic; using System.Dynamic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; public abstract class DerivedTypeJsonConverter<TBase> : JsonConverter<TBase> { protected abstract string TypeToName(Type type); protected abstract Type NameToType(string typeName); private const string TypePropertyName = "$type"; public override bool CanConvert(Type objectType) { return typeof(TBase) == objectType; } public override TBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // get the $type value by parsing the JSON string into a JsonDocument JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader); jsonDocument.RootElement.TryGetProperty(TypePropertyName, out JsonElement typeNameElement); string typeName = (typeNameElement.ValueKind == JsonValueKind.String) ? typeNameElement.GetString() : null; if (string.IsNullOrWhiteSpace(typeName)) throw new InvalidOperationException($"Missing or invalid value for {TypePropertyName} (base type {typeof(TBase).FullName})."); // get the JSON text that was read by the JsonDocument string json; using (var stream = new MemoryStream()) using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Encoder = options.Encoder })) { jsonDocument.WriteTo(writer); writer.Flush(); json = Encoding.UTF8.GetString(stream.ToArray()); } // deserialize the JSON to the type specified by $type try { return (TBase)JsonSerializer.Deserialize(json, NameToType(typeName), options); } catch (Exception ex) { throw new InvalidOperationException("Invalid JSON in request.", ex); } } public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options) { // create an ExpandoObject from the value to serialize so we can dynamically add a $type property to it ExpandoObject expando = ToExpandoObject(value); expando.TryAdd(TypePropertyName, TypeToName(value.GetType())); // serialize the expando JsonSerializer.Serialize(writer, expando, options); } private static ExpandoObject ToExpandoObject(object obj) { var expando = new ExpandoObject(); if (obj != null) { // copy all public properties foreach (PropertyInfo property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead)) { expando.TryAdd(property.Name, property.GetValue(obj)); } } return expando; } }**2)**對於每個基本類型,創建一個派生自
DerivedTypeJsonConverter. 實現 2 個抽象方法,用於將 $type 字元串映射到實際類型。IResultProperty這是您可以遵循的我的界面範例。public class ResultPropertyJsonConverter : DerivedTypeJsonConverter<IResultProperty> { protected override Type NameToType(string typeName) { return typeName switch { // map string values to types nameof(StringResultProperty) => typeof(StringResultProperty) // TODO: Create a case for each derived type }; } protected override string TypeToName(Type type) { // map types to string values if (type == typeof(StringResultProperty)) return nameof(StringResultProperty); // TODO: Create a condition for each derived type } }**3)**在 Startup.cs 中註冊轉換器。
services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.Converters.Add(new ResultPropertyJsonConverter()); // TODO: Add each converter });**4)**在對 API 的請求中,派生類型的對象需要包含 $type 屬性。範例 JSON:
{ "Value":"Hi!", "$type":"StringResultProperty" }
雖然其他答案很好並解決了問題,但如果您想要的只是一般行為類似於 pre netcore3,您可以使用Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包並在 Startup.cs 中執行以下操作:
services.AddControllers().AddNewtonsoftJson()更多資訊在這裡。這樣,您不需要創建任何額外的 json 轉換器。
