web-dev-qa-db-ja.com

カスタムJsonConverter(Web API)からのJson.Net JsonSerializerの自己参照ループ

プロジェクトはAsp.Net Web API Webサービスです。

Jsonとの間でシリアル化できるようにする必要がある型階層があるので、次のSOからコードを取得しました: JSON.NETでカスタムJsonConverterを実装して基本クラスのリストを逆シリアル化する方法オブジェクト? 、そしてコンバーターを階層の基本クラスに適用しました。次のようなものです(ここには、無関係を隠すための疑似コードがあります):

[JsonConverter(typeof(TheConverter))]
public class BaseType
{
    // note the base of this type here is from the linked SO above
    private class TheConverter : JsonCreationConverter<BaseType>
    {
        protected override BaseType Create(Type objectType, JObject jObject)
        {
            Type actualType = GetTypeFromjObject(jObject); /*method elided*/
            return (BaseType)Activator.CreateInstance(actualType);
        }
    }
}

public class RootType
{
    public BaseType BaseTypeMember { get; set; }
}

public class DerivedType : BaseType
{

}

したがって、RootTypeBaseTypeMemberのインスタンスと等しいDerivedTypeインスタンスを逆シリアル化すると、その型のインスタンスに逆シリアル化されます。

レコードの場合、これらのJSONオブジェクトには'$type'フィールド(完全な.Netタイプ名ではない)の仮想タイプ名が含まれるため、シリアル化および非シリアル化できるタイプを正確に制御しながら、JSONでタイプを同時にサポートできます。

これは、リクエストから値をデシリアライズするのに非常によく機能します。シリアル化に問題があります。リンクされたSOを見ると、実際、トップの回答からリンクされているJson.Netのディスカッションを見ると、私が使用しているベースコードは完全に逆シリアル化を対象としていることがわかります。シリアライザの手動作成を示す使用例を示します。これによってJsonConverter実装がテーブルにもたらされましたJsonCreationConverter<T>は単にNotImplementedExceptionをスローします。

さて、Web APIがリクエストに単一のフォーマッタを使用する方法のため、WriteObjectメソッドに「標準」のシリアル化を実装する必要があります。

この時点で、プロジェクトのこの部分に着手する前に、everythingを適切にシリアル化していたことを強調する必要がありますエラーなし

だから私はこれをしました:

public override void WriteJson(JsonWriter writer, 
  object value, 
  JsonSerializer serializer)
{
    serializer.Serialize(writer, value);
}

しかし、私はJsonSerializationExceptionを取得します:Self referencing loop detected with type 'DerivedType'、オブジェクトの1つがシリアル化されるとき。繰り返しますが、コンバーター属性を削除すると(カスタム作成が無効になります)、正常に機能します...

これは、私のシリアライゼーションコードが実際に同じオブジェクトでコンバーターを再度トリガーし、次にシリアライザを再び呼び出すことを意味していると感じています-吐き気です。 確認済み-私の回答を参照してください

では、どのコードWriteObjectで記述すべきかは、機能するのと同じ「標準」シリアル化を行うでしょうか?

39
Andras Zoltan

まあこれは楽しかった...

例外のスタックトレースをさらに詳しく見てみると、メソッドJsonSerializerInternalWriter.SerializeConvertableが2回あることに気付きました。実際、スタックの一番上にあるメソッド1-JsonSerializerInternalWriter.CheckForCircularReferenceの呼び出し-ターンは例外を投げていました。ただし、これは、自分のコンバーターのWriteメソッドの呼び出し元でもありました。

だからシリアライザがやっているように見えるでしょう:

  • 1)オブジェクトにコンバーターがある場合
    • 1a)循環参照の場合はスロー
    • 1b)コンバーターのWriteメソッドを呼び出す
  • 2)その他
    • 2a)内部シリアライザを使用する

したがって、この場合、Json.Netは私のコンバーターを呼び出し、次にJson.Netシリアライザーを呼び出します。Json.Netシリアライザーは、渡されたオブジェクトを既にシリアル化していることがわかるため、爆破します。

DLLでILSpyを開き(はい、それがオープンソースであることはわかっていますが、「呼び出し元」機能が必要です!)、コールスタックをSerializeConvertableからJsonSerializerInternalWriter.SerializeValueに移動します。コンバーターを使用する必要があるかどうかを検出するコードは、最初の方にあります。

if (((jsonConverter = ((member != null) ? member.Converter : null)) != null 
   || (jsonConverter = ((containerProperty != null) ? containerProperty.ItemConverter 
                                                    : null)) != null 
   || (jsonConverter = ((containerContract != null) ? containerContract.ItemConverter 
                                                    : null)) != null 
   || (jsonConverter = valueContract.Converter) != null 
   || (jsonConverter = 
       this.Serializer.GetMatchingConverter(valueContract.UnderlyingType)) != null 
   || (jsonConverter = valueContract.InternalConverter) != null) 
   && jsonConverter.CanWrite)
{
    this.SerializeConvertable(writer, jsonConverter, value, valueContract, 
                              containerContract, containerProperty);
    return;
}

ありがたいことに、ifステートメントの最後の条件が問題の解決策を提供します。リンクSOのコードからコピーしたベースコンバーターのいずれかに以下を追加するだけで済みました。質問、または派生したもの:

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

そして今、それはすべてうまくいきます。

ただし、これの要点は、オブジェクトにカスタムJSONシリアル化を行うつもりで、それをコンバーターand一部またはすべての状況で標準のシリアル化メカニズムにフォールバックする予定です。フレームワークをだまして、循環参照を格納しようとしていると考えてしまうので、それはできません。

私はReferenceLoopHandlingメンバーを操作しようとしましたが、Ignoreに指示した場合、何もシリアル化されず、保存するように指示した場合、当然のことながら、スタックオーバーフローが発生しました。

これはJson.Netのバグである可能性があります-申し訳ありませんが、これはEdgeケースの多くであり、宇宙のエッジから落ちる危険があります-しかし、この状況に陥ると、一種の行き詰まりになります!

53
Andras Zoltan

Newtonsoft.Jsonのバージョン4.5.7.15008を使用してこの問題が発生しました。ここで提供されているすべてのソリューションを他のソリューションとともに試しました。以下のコードを使用して問題を解決しました。基本的には、別のJsonSerializerを使用してシリアル化を実行できます。作成されたJsonSerializerにはコンバーターが登録されていないため、再入/例外は回避されます。他の設定またはContractResolverが使用されている場合、作成されたシリアル化で手動で設定する必要があります。これに対応するために、いくつかのコンストラクター引数をCustomConverterクラスに追加できます。

    public class CustomConverter : JsonConverter
    {
        /// <summary>
        /// Use a privately create serializer so we don't re-enter into CanConvert and cause a Newtonsoft exception
        /// </summary>
        private readonly JsonSerializer noRegisteredConvertersSerializer = new JsonSerializer();

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            bool meetsCondition = false; /* add condition here */
            if (!meetsCondition)
                writer.WriteNull();
            else
                noRegisteredConvertersSerializer.Serialize(writer, value);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }

        public override bool CanConvert(Type objectType)
        {
            // example: register accepted conversion types here
            return typeof(IDictionary<string, object>).IsAssignableFrom(objectType);
        }
    }
7
Ian Gibson

自分でこれに出会ったばかりで、欲求不満で髪を抜いていた!

この問題を解決するために、次の方法でうまくいきましたが、CanWriteのソリューションを見逃したため、より複雑な回避策です。

  • Converterを使用している既存のクラスのコピーを作成し、別の名前を付けます。
  • コピーのJsonConverter属性を削除します。
  • 元のクラスと同じ型のパラメーターを取る新しいクラスでコンストラクターを作成します。コンストラクターを使用して、後のシリアル化に必要な値をコピーします。
  • ConverterのWriteJsonメソッドで、値をダミーの型に変換し、代わりにその型をシリアル化します。

たとえば、これは私の元のクラスに似ています。

[JsonConverter(typeof(MyResponseConverter))]
public class MyResponse
{
    public ResponseBlog blog { get; set; }
    public Post[] posts { get; set; }
}

コピーは次のようになります。

public class FakeMyResponse
{
    public ResponseBlog blog { get; set; }
    public Post[] posts { get; set; }

    public FakeMyResponse(MyResponse response)
    {
        blog = response.blog;
        posts = response.posts;
    }
}

WriteJsonは次のとおりです。

public override void WriteJson(JsonWriter writer, object value,
    JsonSerializer serializer)
{
    if (CanConvert(value.GetType()))
    {
        FakeMyResponse response = new FakeMyResponse((MyResponse)value);
        serializer.Serialize(writer, response);
    }
}

編集:

OPは Expando の使用が別の可能な解決策である可能性があることを指摘しました。これはうまく機能し、新しいクラスを作成する手間を省きますが、DLRサポートにはFramework 4.0以降が必要です。アプローチは、新しいdynamicExpandoObjectを作成してから、そのプロパティをWriteJsonメソッドで直接初期化してコピーを作成することです。

public override void WriteJson(JsonWriter writer, object value,
    JsonSerializer serializer)
{
    if (CanConvert(value.GetType()))
    {
        var response = (MyResponse)value;
        dynamic fake = new System.Dynamic.ExpandoObject();
        fake.blog = response.blog;
        fake.posts = response.posts;
        serializer.Serialize(writer, fake);
    }
}
2
Dave R.

IMO、これはライブラリの深刻な制限です。解決策は非常に単純ですが、すぐには理解できなかったことを認めます。解決策は以下を設定することです:

.ReferenceLoopHandling = ReferenceLoopHandling.Serialize

これは、至る所で説明されているように、自己参照エラーを排除し、スタックオーバーフローで置き換えます。私の場合、書き込み機能が必要だったため、CanWriteをfalseに設定することはできませんでした。最後に、シリアライザーの呼び出しが原因で(無限の)再帰が発生することがわかっている場合は、CanConvert呼び出しを保護するためのフラグを設定するだけです。

    Public Class ReferencingObjectConverter : Inherits JsonConverter

        Private _objects As New HashSet(Of String)
        Private _ignoreNext As Boolean = False

        Public Overrides Function CanConvert(objectType As Type) As Boolean
            If Not _ignoreNext Then
                Return GetType(IElement).IsAssignableFrom(objectType) AndAlso Not GetType(IdProperty).IsAssignableFrom(objectType)
            Else
                _ignoreNext = False
                Return False
            End If
        End Function

        Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)

            Try
                If _objects.Contains(CType(value, IElement).Id.Value) Then 'insert a reference to existing serialized object
                    serializer.Serialize(writer, New Reference With {.Reference = CType(value, IElement).Id.Value})
                Else 'add to my list of processed objects
                    _objects.Add(CType(value, IElement).Id.Value)
                    'the serialize will trigger a call to CanConvert (which is how we got here it the first place)
                    'and will bring us right back here with the same 'value' parameter (and SO eventually), so flag
                    'the CanConvert function to skip the next call.
                    _ignoreNext = True
                    serializer.Serialize(writer, value)
                End If
            Catch ex As Exception
                Trace.WriteLine(ex.ToString)
            End Try

        End Sub

        Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
            Throw New NotImplementedException()
        End Function

        Private Class Reference
            Public Property Reference As String
        End Class

    End Class
1
2stroke

私は親/子コレクションで同じ問題を抱えていて、私の記事を解決した投稿を見つけました。親コレクションアイテムのリストを表示したいだけで、子データは必要なかったので、次を使用するとうまくいきました。

JsonConvert.SerializeObject(ResultGroups, Formatting.None,
                        new JsonSerializerSettings()
                        { 
                            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                        });

また、次のJson.NET codplexページも参照しています。

http://json.codeplex.com/discussions/272371

これは誰かを助けるかもしれませんが、私の場合、Equalsメソッドをオーバーライドして、オブジェクトを値型として扱うようにしました。私の調査で、JSON.NETは次のようになっていないことがわかりました。

JSON.NET自己参照エラー

0
ProVega

鉱山は単純な間違いであり、このトピックの解決策とは何の関係もありませんでした。

このトピックはグーグルの最初のページだったので、他の人が私と同じ問題を抱えている場合に備えて、ここに投稿します。

dynamic table = new ExpandoObject();
..
..
table.rows = table; <<<<<<<< I assigned same dynamic object to itself. 
0
dvdmn