来自 ASP.NET 核心 API 的 JSON 响应中缺少派生类型的属性
Derived type's properties missing in JSON response from ASP.NET Core API
我的 ASP.NET Core 3.1 API 控制器的 JSON 响应缺少属性。当 属性 使用派生类型时会发生这种情况;在派生类型中定义但未在 base/interface 中定义的任何属性都不会序列化为 JSON。响应中似乎缺少对多态性的支持,好像序列化是基于 属性 的定义类型而不是其运行时类型。如何更改此行为以确保所有 public 属性都包含在 JSON 响应中?
示例:
我的 .NET Core Web API 控制器 return 这个对象有一个 属性 接口类型。
// controller returns this object
public class Result
{
public IResultProperty ResultProperty { get; set; } // property uses an interface type
}
public interface IResultProperty
{ }
这里是一个派生类型,它定义了一个名为 Value
.
的新 public 属性
public class StringResultProperty : IResultProperty
{
public string Value { get; set; }
}
如果我 return 来自我的控制器的派生类型是这样的:
return new MainResult {
ResultProperty = new StringResultProperty { Value = "Hi there!" }
};
然后实际响应包含一个空对象(缺少 Value
属性):
我希望回复是:
{
"ResultProperty": { "Value": "Hi there!" }
}
这是预期的结果。当你这样做时你正在向上转换,所以将被序列化的是被向上转换的对象,而不是实际的派生类型。如果您需要来自派生类型的东西,那么它必须是 属性 的类型。出于这个原因,您可能想要使用泛型。换句话说:
public class Result<TResultProperty>
where TResultProperty : IResultProperty
{
public TResultProperty ResultProperty { get; set; } // property uses an interface type
}
然后:
return new Result<StringResultProperty> {
ResultProperty = new StringResultProperty { Value = "Hi there!" }
};
我最终创建了一个自定义 JsonConverter
(System.Text.Json.Serialization 命名空间),它强制 JsonSerializer
序列化到 object 的 运行时 类型。请参阅下面的解决方案部分。它很长,但效果很好,不需要我在 API 的设计中牺牲面向 object 的原则。 (如果您需要更快的东西并且可以使用 Newtonsoft,那么请查看投票最高的答案。)
一些背景: Microsoft 有一个 System.Text.Json
序列化指南,其中有一个标题为 Serialize properties of derived classes 的部分,其中包含与我的问题相关的有用信息。特别是它解释了为什么派生类型的属性没有被序列化:
This behavior is intended to help prevent accidental exposure of data
in a derived runtime-created type.
如果这不是您关心的问题,那么可以通过显式指定派生类型或指定 object
来覆盖对 JsonSerializer.Serialize
的调用,例如:
// 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 上述方法之一。我还实现了对 deserialization 的支持,虽然在原始问题中没有明确要求,但几乎总是需要。 (奇怪的是,无论如何,仅支持序列化而不支持反序列化被证明是棘手的。)
解决方案
我创建了一个基础 class、DerivedTypeJsonConverter
,其中包含所有序列化和反序列化逻辑。对于每个基本类型,您将为它创建一个对应的转换器 class,它派生自 DerivedTypeJsonConverter
。这在下面的编号方向中进行了解释。
此解决方案遵循 Json.NET 的 "type name handling" 约定,该约定将对多态性的支持引入 JSON。它的工作原理是在派生类型的 JSON(例如:"$type":"StringResultProperty"
)中包含一个额外的 $type 属性,告诉转换器 object 的真实类型是。 (一个区别:在 Json.NET 中,$type 的值是一个完全限定类型 + 程序集名称,而我的 $type 是一个自定义字符串,它帮助 future-proof 防止 namespace/assembly/class 名称更改。) API 调用者应在其 JSON 对派生类型的请求中包含 $type 属性。序列化逻辑通过确保所有 object 的 public 属性都被序列化来解决我原来的问题,并且为了保持一致性,$type 属性 也被序列化。
路线:
1) 将下面的 DerivedTypeJsonConverter class 复制到您的项目中。
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
的 class。实现用于将 $type 字符串映射到实际类型的 2 个抽象方法。这是我的 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、object 派生类型的请求中,需要包含 $type 属性。示例 JSON:{ "Value":"Hi!", "$type":"StringResultProperty" }
The documentation 展示了在直接调用序列化程序时如何序列化为派生的 class。同样的技术也可以用在自定义转换器中,然后我们可以用它来标记我们的 classes。
首先,创建一个自定义转换器
public class AsRuntimeTypeConverter<T> : JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<T>(ref reader, options);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value?.GetType() ?? typeof(object), options);
}
}
然后将相关的class标记为与新转换器一起使用
[JsonConverter(typeof(AsRuntimeTypeConverter<MyBaseClass>))]
public class MyBaseClass
{
...
或者,可以在 startup.cs 中注册转换器
services
.AddControllers(options =>
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new AsRuntimeTypeConverter<MyBaseClass>());
}));
虽然其他答案很好并且解决了问题,但如果您只想要像 netcore3 之前的一般行为,您可以使用 Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包并在 Startup.cs 中执行:
services.AddControllers().AddNewtonsoftJson()
更多信息here。这样,您不需要创建任何额外的 json-转换器。
我通过写这个扩展解决了它:
public static class JsonSerializationExtensions
{
public static string ToJson<T>(this IEnumerable<T> enumerable, bool includeDerivedTypesProperties = true)
where T : class
{
var jsonOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
if (includeDerivedTypeProperties)
{
var collection = enumerable.Select(e => e as object).ToList();
return JsonSerializer.Serialize<object>(collection, jsonOptions);
}
else
{
return JsonSerializer.Serialize(enumerable, jsonOptions);
}
}
}
我也在 .NET Core 3.1 API 中苦苦挣扎,我希望结果包含 $type 属性。
按照建议,安装正确的包,然后 'AddNewtonsoftJson'。
我想添加 $type 字段以显示派生类型处理,以实现
services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.All;
});
没有敲 Newtonsoft,但我发现了一种使用内置处理程序解决此问题的更简单方法。
[OperationContract]
[WebInvoke(Method = "GET", UriTemplate = "/emps", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
List<emp> GetEmps();
//[DataContract(Namespace = "foo")] <<< comment/removed this line
public class emp
{
public string userId { get; set; }
public string firstName { get; set; }
}
public class dept
{
public string deptId{ get; set; }
public string deptName{ get; set; }
}
在我的例子中,dept 对象工作正常,但 emp 对象不是——它们看起来是空的。
我有一个类似的问题,我返回了一个 TAnimal
类型的枚举(但对象实例是派生类型,例如 Dog
、Cat
等) :
[HttpGet]
public IEnumerable<TAnimal> GetAnimals()
{
IEnumerable<TAnimal> list = GetListOfAnimals();
return list;
}
这仅包括 TAnimal
中定义的属性。
但是,至少在 ASP .NET Core 3.1 中,我发现我可以将对象实例转换为 object
,然后 JSON 序列化程序包含所有属性来自派生 类:
[HttpGet]
public IEnumerable<object> GetAnimals()
{
IEnumerable<TAnimal> list = GetListOfAnimals();
return list.Select(a => (object)a);
}
(请注意,GetAnimals
方法的签名也必须更改,但这在 Web API 上下文中通常无关紧要)。如果你需要为 Swagger 或其他什么提供类型信息,你可以注释该方法:
[HttpGet]
[Produces(MediaTypeNames.Application.Json, Type = typeof(TAnimal[]))]
public IEnumerable<object> GetAnimals()
{
...
}
如果您只需要担心 1-layer-deep 对象层次结构,则转换为 object
是一个简单的解决方案。
我的 ASP.NET Core 3.1 API 控制器的 JSON 响应缺少属性。当 属性 使用派生类型时会发生这种情况;在派生类型中定义但未在 base/interface 中定义的任何属性都不会序列化为 JSON。响应中似乎缺少对多态性的支持,好像序列化是基于 属性 的定义类型而不是其运行时类型。如何更改此行为以确保所有 public 属性都包含在 JSON 响应中?
示例:
我的 .NET Core Web API 控制器 return 这个对象有一个 属性 接口类型。
// 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 来自我的控制器的派生类型是这样的:
return new MainResult {
ResultProperty = new StringResultProperty { Value = "Hi there!" }
};
然后实际响应包含一个空对象(缺少 Value
属性):
我希望回复是:
{
"ResultProperty": { "Value": "Hi there!" }
}
这是预期的结果。当你这样做时你正在向上转换,所以将被序列化的是被向上转换的对象,而不是实际的派生类型。如果您需要来自派生类型的东西,那么它必须是 属性 的类型。出于这个原因,您可能想要使用泛型。换句话说:
public class Result<TResultProperty>
where TResultProperty : IResultProperty
{
public TResultProperty ResultProperty { get; set; } // property uses an interface type
}
然后:
return new Result<StringResultProperty> {
ResultProperty = new StringResultProperty { Value = "Hi there!" }
};
我最终创建了一个自定义 JsonConverter
(System.Text.Json.Serialization 命名空间),它强制 JsonSerializer
序列化到 object 的 运行时 类型。请参阅下面的解决方案部分。它很长,但效果很好,不需要我在 API 的设计中牺牲面向 object 的原则。 (如果您需要更快的东西并且可以使用 Newtonsoft,那么请查看投票最高的答案。)
一些背景: Microsoft 有一个 System.Text.Json
序列化指南,其中有一个标题为 Serialize properties of derived classes 的部分,其中包含与我的问题相关的有用信息。特别是它解释了为什么派生类型的属性没有被序列化:
This behavior is intended to help prevent accidental exposure of data in a derived runtime-created type.
如果这不是您关心的问题,那么可以通过显式指定派生类型或指定 object
来覆盖对 JsonSerializer.Serialize
的调用,例如:
// 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 上述方法之一。我还实现了对 deserialization 的支持,虽然在原始问题中没有明确要求,但几乎总是需要。 (奇怪的是,无论如何,仅支持序列化而不支持反序列化被证明是棘手的。)
解决方案
我创建了一个基础 class、DerivedTypeJsonConverter
,其中包含所有序列化和反序列化逻辑。对于每个基本类型,您将为它创建一个对应的转换器 class,它派生自 DerivedTypeJsonConverter
。这在下面的编号方向中进行了解释。
此解决方案遵循 Json.NET 的 "type name handling" 约定,该约定将对多态性的支持引入 JSON。它的工作原理是在派生类型的 JSON(例如:"$type":"StringResultProperty"
)中包含一个额外的 $type 属性,告诉转换器 object 的真实类型是。 (一个区别:在 Json.NET 中,$type 的值是一个完全限定类型 + 程序集名称,而我的 $type 是一个自定义字符串,它帮助 future-proof 防止 namespace/assembly/class 名称更改。) API 调用者应在其 JSON 对派生类型的请求中包含 $type 属性。序列化逻辑通过确保所有 object 的 public 属性都被序列化来解决我原来的问题,并且为了保持一致性,$type 属性 也被序列化。
路线:
1) 将下面的 DerivedTypeJsonConverter class 复制到您的项目中。
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
的 class。实现用于将 $type 字符串映射到实际类型的 2 个抽象方法。这是我的 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、object 派生类型的请求中,需要包含 $type 属性。示例 JSON:{ "Value":"Hi!", "$type":"StringResultProperty" }
The documentation 展示了在直接调用序列化程序时如何序列化为派生的 class。同样的技术也可以用在自定义转换器中,然后我们可以用它来标记我们的 classes。
首先,创建一个自定义转换器
public class AsRuntimeTypeConverter<T> : JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<T>(ref reader, options);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value?.GetType() ?? typeof(object), options);
}
}
然后将相关的class标记为与新转换器一起使用
[JsonConverter(typeof(AsRuntimeTypeConverter<MyBaseClass>))]
public class MyBaseClass
{
...
或者,可以在 startup.cs 中注册转换器
services
.AddControllers(options =>
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new AsRuntimeTypeConverter<MyBaseClass>());
}));
虽然其他答案很好并且解决了问题,但如果您只想要像 netcore3 之前的一般行为,您可以使用 Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包并在 Startup.cs 中执行:
services.AddControllers().AddNewtonsoftJson()
更多信息here。这样,您不需要创建任何额外的 json-转换器。
我通过写这个扩展解决了它:
public static class JsonSerializationExtensions
{
public static string ToJson<T>(this IEnumerable<T> enumerable, bool includeDerivedTypesProperties = true)
where T : class
{
var jsonOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
if (includeDerivedTypeProperties)
{
var collection = enumerable.Select(e => e as object).ToList();
return JsonSerializer.Serialize<object>(collection, jsonOptions);
}
else
{
return JsonSerializer.Serialize(enumerable, jsonOptions);
}
}
}
我也在 .NET Core 3.1 API 中苦苦挣扎,我希望结果包含 $type 属性。
按照建议,安装正确的包,然后 'AddNewtonsoftJson'。
我想添加 $type 字段以显示派生类型处理,以实现
services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.All;
});
没有敲 Newtonsoft,但我发现了一种使用内置处理程序解决此问题的更简单方法。
[OperationContract]
[WebInvoke(Method = "GET", UriTemplate = "/emps", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
List<emp> GetEmps();
//[DataContract(Namespace = "foo")] <<< comment/removed this line
public class emp
{
public string userId { get; set; }
public string firstName { get; set; }
}
public class dept
{
public string deptId{ get; set; }
public string deptName{ get; set; }
}
在我的例子中,dept 对象工作正常,但 emp 对象不是——它们看起来是空的。
我有一个类似的问题,我返回了一个 TAnimal
类型的枚举(但对象实例是派生类型,例如 Dog
、Cat
等) :
[HttpGet]
public IEnumerable<TAnimal> GetAnimals()
{
IEnumerable<TAnimal> list = GetListOfAnimals();
return list;
}
这仅包括 TAnimal
中定义的属性。
但是,至少在 ASP .NET Core 3.1 中,我发现我可以将对象实例转换为 object
,然后 JSON 序列化程序包含所有属性来自派生 类:
[HttpGet]
public IEnumerable<object> GetAnimals()
{
IEnumerable<TAnimal> list = GetListOfAnimals();
return list.Select(a => (object)a);
}
(请注意,GetAnimals
方法的签名也必须更改,但这在 Web API 上下文中通常无关紧要)。如果你需要为 Swagger 或其他什么提供类型信息,你可以注释该方法:
[HttpGet]
[Produces(MediaTypeNames.Application.Json, Type = typeof(TAnimal[]))]
public IEnumerable<object> GetAnimals()
{
...
}
如果您只需要担心 1-layer-deep 对象层次结构,则转换为 object
是一个简单的解决方案。