web-dev-qa-db-ja.com

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プロパティがありません)。

enter image description here

私は応答が欲しいです:

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

結局、JsonSerializerにオブジェクトのruntimeタイプにシリアル化させるカスタムJsonConverter(System.Text.Json.Serialization名前空間)を作成することになりました。以下のソリューションのセクションを参照してください。それは長いですが、うまく機能し、APIの設計でオブジェクト指向の原則を犠牲にする必要はありません。

背景:Microsoftには、System.Text.Jsonシリアル化ガイドと題されたセクション 派生クラスのプロパティのシリアル化 があり、情報が豊富です私の質問に関連しています。特に、派生型のプロパティがシリアル化されない理由を説明します。

この動作は、ランタイムが作成した派生型のデータが誤って公開されるのを防ぐことを目的としています。

これが問題にならない場合は、派生型を明示的に指定するか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でこれを実現するには、シリアル化プロセスにフックする必要があります。私は、JsonSerializer.Serializeを上記のいずれかの方法で呼び出すカスタムJsonConverterでこれを行いました。 deserializationのサポートも実装しました。元の質問では明示的に要求されていませんが、とにかくほとんど常に必要です。 (奇妙なことに、シリアル化のみをサポートし、逆シリアル化をサポートしないことはとにかく難しいことが判明しました。)

ソリューション

基本クラスDerivedTypeJsonConverterを作成しました。これには、シリアライゼーションとデシリアライゼーションのすべてのロジックが含まれています。各基本型について、DerivedTypeJsonConverterから派生する対応するコンバータークラスを作成します。これは、以下の番号の付いた方向で説明されています。

このソリューションは、JSONへのポリモーフィズムのサポートを導入するJson.NETの "type name Handling" 規則に従います。派生型のJSON(例:"$type":"StringResultProperty")に追加の$ typeプロパティを含めることで機能し、コンバーターにオブジェクトの実際の型を通知しますです。 (1つの違い: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から派生するクラスを作成します。 mappig $ 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へのリクエストでは、派生型のオブジェクトに$ typeプロパティを含める必要があります。 JSONの例:{ "Value":"Hi!", "$type":"StringResultProperty" }

ここで完全な要点

5
Keith

上記の回答は適切で問題を解決しますが、一般的な動作をnetcore3より前のようにしたい場合は、 Microsoft.AspNetCore.Mvc.NewtonsoftJson nugetパッケージを使用して、Startup.csで行うことができます:

services.AddControllers().AddNewtonsoftJson()

詳細 ここ 。この方法では、追加のjson-converterを作成する必要はありません。

1
Fredrik Ek

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>());
            }));
1
nimatt

microsoft.AspNetCore.Mvc.NewtonsoftJson nugetパッケージを使用し、

services.AddControllers().AddNewtonsoftJson()

私のapiプロジェクトでは、Startup.csも機能しました。追加のコードは不要であり、Fredrik Ekです。

0
Bruce Holman

これは期待される結果です。これを行うとアップキャストされます。したがって、シリアル化されるのは、実際の派生型ではなく、アップキャストされたオブジェクトです。派生型のものが必要な場合は、それがプロパティの型でなければなりません。このため、ジェネリックを使用することをお勧めします。言い換えると:

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