来自 ASP.NET Core API 的 JSON 响应中缺少派生类型的属性

Kei*_*ith 23 .net-core asp.net-core .net-core-3.1

来自我的 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
    { }
Run Code Online (Sandbox Code Playgroud)

这是一个派生类型,它定义了一个名为 的新公共属性Value

    public class StringResultProperty : IResultProperty
    {
        public string Value { get; set; }
    }
Run Code Online (Sandbox Code Playgroud)

如果我像这样从控制器返回派生类型:

    return new MainResult {
        ResultProperty = new StringResultProperty { Value = "Hi there!" }
    };
Run Code Online (Sandbox Code Playgroud)

那么实际的响应包括一个空对象(该Value属性丢失):

在此处输入图片说明

我希望得到的回应是:

    {
        "ResultProperty": { "Value": "Hi there!" }
    }
Run Code Online (Sandbox Code Playgroud)

Fre*_* Ek 18

虽然其他答案很好并解决了问题,但如果您想要的只是像 pre netcore3 一样的一般行为,您可以使用Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包并在 Startup.cs 中执行以下操作:

services.AddControllers().AddNewtonsoftJson()
Run Code Online (Sandbox Code Playgroud)

更多信息在这里。这样,您无需创建任何额外的 json 转换器。


Kei*_*ith 16

我最终创建了一个自定义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);
Run Code Online (Sandbox Code Playgroud)

要使用 ASP.NET Core 完成此操作,您需要挂钩序列化过程。我用一个自定义的 JsonConverter 来做到这一点,它调用 JsonSerializer.Serialize 上面显示的方法之一。我还实现了对反序列化的支持,虽然在原始问题中没有明确要求,但无论如何几乎总是需要的。(奇怪的是,无论如何,只支持序列化而不支持反序列化被证明是很棘手的。)

解决方案

我创建了一个基类,DerivedTypeJsonConverter它包含所有的序列化和反序列化逻辑。对于每个基本类型,您将为它创建一个相应的转换器类,该类派生自DerivedTypeJsonConverter. 这在下面的编号方向中进行了解释。

此解决方案遵循Json.NET的“类型名称处理”约定,该约定引入了对 JSON 的多态性支持。它的工作原理是在派生类型的 JSON(例如:)中包含一个额外的$type属性,该属性"$type":"StringResultProperty"告诉转换器对象的真实类型是什么。(一个区别:在 Json.NET 中,$type 的值是一个完全限定的类型 + 程序集名称,而我的 $type 是一个自定义字符串,它有助于防止命名空间/程序集/类名称更改的未来。)预计 API 调用者包括$type 属性在他们对派生类型的 JSON 请求中。序列化逻辑通过确保所有对象的公共属性都被序列化来解决我原来的问题,并且为了一致性,$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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

2)对于每个基本类型,创建一个派生自DerivedTypeJsonConverter. 实现用于将 $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
        }
    }
Run Code Online (Sandbox Code Playgroud)

3)在 Startup.cs 中注册转换器。

    services.AddControllers()
        .AddJsonOptions(options => {
            options.JsonSerializerOptions.Converters.Add(new ResultPropertyJsonConverter());

            // TODO: Add each converter
        });
Run Code Online (Sandbox Code Playgroud)

4)在对 API 的请求中,派生类型的对象需要包含 $type 属性。示例 JSON:{ "Value":"Hi!", "$type":"StringResultProperty" }

完整要点在这里

  • 这是 System.Text.Json 的一个非常令人失望的警告。好旧的 Newtonsoft 听起来容易多了 (3认同)
  • 赞成,也在努力解决这个问题。另外,我很确定您知道这一点,但另一种选择是使用 Newtonsoft.Json,它提供对开箱即用的派生类型的支持。 (2认同)
  • 他们为什么不简单地为此提供一个设置呢? (2认同)
  • https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism (2认同)

nim*_*att 10

该文档显示了如何在直接调用序列化程序时将其序列化为派生类。同样的技术也可以用在自定义转换器中,然后我们可以用它来标记我们的类。

首先,创建一个自定义转换器

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后标记要与新转换器一起使用的相关类

[JsonConverter(typeof(AsRuntimeTypeConverter<MyBaseClass>))]
public class MyBaseClass
{
   ...
Run Code Online (Sandbox Code Playgroud)

或者,转换器可以在 startup.cs 中注册

services
  .AddControllers(options =>
     .AddJsonOptions(options =>
            {
                options.JsonSerializerOptions.Converters.Add(new AsRuntimeTypeConverter<MyBaseClass>());
            }));
Run Code Online (Sandbox Code Playgroud)

  • 当然看起来更干净,但在序列化“MyBaseClass”实例时在我的应用程序中导致了“StackOverflowException”,因为调用了“writer.Write”方法,该方法返回到“AsRuntimeTypeConverter&lt;T&gt;”类型。 (2认同)