Json.NET、シリアル化をカスタマイズしてJSONプロパティを挿入する方法 質問する

Json.NET、シリアル化をカスタマイズしてJSONプロパティを挿入する方法 質問する

特定の型をシリアル化するときに JSON プロパティを挿入できる適切な実装を見つけることができませんでしたJsonConvert.WriteJson。すべての試行で、「JsonSerializationException: 型 XXX で自己参照ループが検出されました」という結果になりました。

私が解決しようとしている問題の背景をもう少し説明します。私は JSON を構成ファイル形式として使用しており、 を使用してJsonConverter構成タイプのタイプ解決、シリアル化、および逆シリアル化を制御しています。 プロパティを使用する代わりに$type、正しいタイプを解決するために使用される、より意味のある JSON 値を使用したいと考えています。

簡略化した例として、JSON テキストを以下に示します。

{
  "Target": "B",
  "Id": "foo"
}

ここで、JSON プロパティは、"Target": "B"このオブジェクトを 型にシリアル化する必要があるかどうかを決定するために使用されますB。この設計は、単純な例を考えるとそれほど魅力的ではないかもしれませんが、構成ファイル形式をより使いやすくします。

また、設定ファイルをラウンドトリップ可能にしたいです。デシリアライズの場合は動作していますが、シリアライズの場合は動作しません。

JsonConverter.WriteJson私の問題の根本は、標準の JSON シリアル化ロジックを使用し、「自己参照ループ」例外をスローしない実装が見つからないことです。これが私の実装です:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

    //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
    // Same error occurs whether I use the serializer parameter or a separate serializer.
    JObject jo = JObject.FromObject(value, serializer); 
    if (typeHintProperty != null)
    {
        jo.AddFirst(typeHintProperty);
    }
    writer.WriteToken(jo.CreateReader());
}

これはJson.NETのバグのように思えます。なぜなら、これを行う方法があるはずだからです。残念ながら、JsonConverter.WriteJson私が遭遇したすべての例(例:JSON.NET での特定のオブジェクトのカスタム変換) は、JsonWriter メソッドを使用して個々のオブジェクトとプロパティを書き出すことで、特定のクラスのカスタム シリアル化のみを提供します。

これが私の問題を示すxunitテストの完全なコードです(またはこちらをご覧ください

using System;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

using Xunit;


public class A
{
    public string Id { get; set; }
    public A Child { get; set; }
}

public class B : A {}

public class C : A {}

/// <summary>
/// Shows the problem I'm having serializing classes with Json.
/// </summary>
public sealed class JsonTypeConverterProblem
{
    [Fact]
    public void ShowSerializationBug()
    {
        A a = new B()
              {
                  Id = "foo",
                  Child = new C() { Id = "bar" }
              };

        JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
        jsonSettings.ContractResolver = new TypeHintContractResolver();
        string json = JsonConvert.SerializeObject(a, Formatting.Indented, jsonSettings);
        Console.WriteLine(json);

        Assert.Contains(@"""Target"": ""B""", json);
        Assert.Contains(@"""Is"": ""C""", json);
    }

    [Fact]
    public void DeserializationWorks()
    {
        string json =
@"{
  ""Target"": ""B"",
  ""Id"": ""foo"",
  ""Child"": { 
        ""Is"": ""C"",
        ""Id"": ""bar"",
    }
}";

        JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
        jsonSettings.ContractResolver = new TypeHintContractResolver();
        A a = JsonConvert.DeserializeObject<A>(json, jsonSettings);

        Assert.IsType<B>(a);
        Assert.IsType<C>(a.Child);
    }
}

public class TypeHintContractResolver : DefaultContractResolver
{
    public override JsonContract ResolveContract(Type type)
    {
        JsonContract contract = base.ResolveContract(type);
        if ((contract is JsonObjectContract)
            && ((type == typeof(A)) || (type == typeof(B))) ) // In the real implementation, this is checking against a registry of types
        {
            contract.Converter = new TypeHintJsonConverter(type);
        }
        return contract;
    }
}


public class TypeHintJsonConverter : JsonConverter
{
    private readonly Type _declaredType;

    public TypeHintJsonConverter(Type declaredType)
    {
        _declaredType = declaredType;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == _declaredType;
    }

    // The real implementation of the next 2 methods uses reflection on concrete types to determine the declaredType hint.
    // TypeFromTypeHint and TypeHintPropertyForType are the inverse of each other.

    private Type TypeFromTypeHint(JObject jo)
    {
        if (new JValue("B").Equals(jo["Target"]))
        {
            return typeof(B);
        }
        else if (new JValue("A").Equals(jo["Hint"]))
        {
            return typeof(A);
        }
        else if (new JValue("C").Equals(jo["Is"]))
        {
            return typeof(C);
        }
        else
        {
            throw new ArgumentException("Type not recognized from JSON");
        }
    }

    private JProperty TypeHintPropertyForType(Type type)
    {
        if (type == typeof(A))
        {
            return new JProperty("Hint", "A");
        }
        else if (type == typeof(B))
        {
            return new JProperty("Target", "B");
        }
        else if (type == typeof(C))
        {
            return new JProperty("Is", "C");
        }
        else
        {
            return null;
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (! CanConvert(objectType))
        {
            throw new InvalidOperationException("Can't convert declaredType " + objectType + "; expected " + _declaredType);
        }

        // Load JObject from stream.  Turns out we're also called for null arrays of our objects,
        // so handle a null by returning one.
        var jToken = JToken.Load(reader);
        if (jToken.Type == JTokenType.Null)
            return null;
        if (jToken.Type != JTokenType.Object)
        {
            throw new InvalidOperationException("Json: expected " + _declaredType + "; got " + jToken.Type);
        }
        JObject jObject = (JObject) jToken;

        // Select the declaredType based on TypeHint
        Type deserializingType = TypeFromTypeHint(jObject);

        var target = Activator.CreateInstance(deserializingType);
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

    public override bool CanWrite
    {
        get { return true; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

        //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
        // Same error occurs whether I use the serializer parameter or a separate serializer.
        JObject jo = JObject.FromObject(value, serializer); 
        if (typeHintProperty != null)
        {
            jo.AddFirst(typeHintProperty);
        }
        writer.WriteToken(jo.CreateReader());
    }

}

ベストアンサー1

変換される同じオブジェクトに対してコンバーター内から呼び出すとJObject.FromObject()、ご覧のとおり、再帰ループが発生します。通常、解決策は、(a) コンバーター内で別の JsonSerializer インスタンスを使用するか、(b) James が回答で指摘したように、プロパティを手動でシリアル化することです。あなたのケースは少し特殊で、どちらの解決策も実際には機能しません。コンバーターを認識しない別のシリアライザー インスタンスを使用すると、子オブジェクトにヒント プロパティが適用されません。また、コメントで述べたように、完全に手動でシリアル化することは、一般的な解決策としては機能しません。

幸いなことに、中間の解決策があります。メソッド内でリフレクションを少し使用してWriteJsonオブジェクトのプロパティを取得し、そこから に委譲することができますJToken.FromObject()。コンバーターは、子プロパティに対しては再帰的に呼び出されますが、現在のオブジェクトに対しては呼び出されないため、問題が発生することはありません。このソリューションには 1 つの注意点があります。[JsonProperty]このコンバーターによって処理されるクラス (例では A、B、C) に属性が適用されている場合、それらの属性は考慮されません。

メソッドの更新されたコードは次のとおりですWriteJson

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

    JObject jo = new JObject();
    if (typeHintProperty != null)
    {
        jo.Add(typeHintProperty);
    }
    foreach (PropertyInfo prop in value.GetType().GetProperties())
    {
        if (prop.CanRead)
        {
            object propValue = prop.GetValue(value);
            if (propValue != null)
            {
                jo.Add(prop.Name, JToken.FromObject(propValue, serializer));
            }
        }
    }
    jo.WriteTo(writer);
}

フィドル:https://dotnetfiddle.net/jQrxb8

おすすめ記事