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

时间:2019-12-12 16:17:07

标签: asp.net-core .net-core .net-core-3.1

我的ASP.NET Core 3.1 API控制器的JSON响应缺少属性。当属性使用派生类型时,就会发生这种情况。在派生类型中定义但在基类/接口中没有定义的任何属性都不会序列化为JSON。似乎在响应中缺少对多态性的支持,好像序列化是基于属性的 defined 类型而不是其 actual 类型。如何更改此行为,以确保所有公共属性都包含在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):

enter image description here

我希望回复为:

{
    "ResultProperty": { "Value": "Hi there!" }
}

6 个答案:

答案 0 :(得分:9)

虽然其他答案很好并且可以解决问题,但是如果您想要的只是像netcore3之前的一般行为,则可以使用Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet包,并在Startup.cs中这样做:

services.AddControllers().AddNewtonsoftJson()

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

答案 1 :(得分:5)

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

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

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);
    }
}

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

[JsonConverter(typeof(AsRuntimeTypeConverter<MyBaseClass>))]
public class MyBaseClass
{
   ...

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

services
  .AddControllers(options =>
     .AddJsonOptions(options =>
            {
                options.JsonSerializerOptions.Converters.Add(new AsRuntimeTypeConverter<MyBaseClass>());
            }));

答案 2 :(得分:0)

这是预期的结果。当您执行此操作时,您将进行向上转换,因此要序列化的是向上转换的对象,而不是实际的派生类型。如果需要派生类型的内容,则必须是属性的类型。因此,您可能要使用泛型。换句话说:

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!" }  
};

答案 3 :(得分:0)

我最终创建了一个自定义JsonConverter(System.Text.Json.Serialization命名空间),该强制JsonSerializer序列化为对象的 runtime 类型。请参阅下面的“解决方案”部分。它很长,但是效果很好,不需要我在设计API响应时牺牲面向对象的原理。

某些背景:Microsoft有System.Text.Json序列化指南,标题为Serialize properties of derived classes,其中包含与我的问题相关的良好信息。特别是它解释了为什么未对派生类型的属性进行序列化的原因:

  

此行为旨在帮助防止意外泄露数据   以派生的运行时创建的类型。

如果您不必担心,则可以通过显式指定派生类型或通过指定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实现此目的,您需要加入序列化过程。我通过调用JsonSerializer的自定义JsonConverter做到了这一点。序列化上面显示的方法之一。我还实现了对反序列化的支持,尽管在原始问题中没有明确要求,但无论如何几乎都需要。 (奇怪的是,无论如何,仅支持序列化而不支持反序列化确实很棘手。)

解决方案

有一个基类DerivedTypeJsonConverter,其中包含所有序列化和反序列化逻辑。对于每种基本类型,为其创建一个从DerivedTypeJsonConverter派生的转换器类。

此解决方案遵循Json.NET的"type name handling"约定,该约定引入了对JSON的多态性的支持。它通过在派生类型的JSON(例如:"$type":"StringResultProperty")中包含一个附加的 $ type 属性来工作,该属性告诉转换器对象的真实类型是什么。 (一个区别:在Json.NET中,$ type的值是完全限定的类型+程序集名称,而我的$ type是一个自定义字符串,有助于将来防止名称空间/程序集/类名更改。)调用者应包括$ 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值映射到类型。请遵循下面的ResultPropertyJsonConverter示例。

public class ResultPropertyJsonConverter : DerivedTypeJsonConverter<IResultProperty>
{
    protected override Type NameToType(string typeName)
    {
        return typeName switch
        {
            // if "StringResultProperty" then return type StringResultProperty
            nameof(StringResultProperty) => typeof(StringResultProperty)
            // TODO: Create a case for each derived type
        };
    }

    protected override string TypeToName(Type type)
    {
        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" }

Full gist here

答案 4 :(得分:0)

我通过编写此扩展程序解决了该问题:

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);
        }
    }
}

答案 5 :(得分:0)

我也在.NET Core 3.1 API中为此苦苦挣扎,我希望结果包含$ type属性。

根据建议,安装正确的软件包,然后安装“ AddNewtonsoftJson”。

我希望添加$ type字段以显示派生的类型处理,以获取该信息

services.AddControllers().AddNewtonsoftJson(options => 
{ 
    options.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.All;
});