来自 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" }

Full gist here

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 类型的枚举(但对象实例是派生类型,例如 DogCat 等) :

[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 是一个简单的解决方案。