2

カスタム参照ループ処理を実装しようとしています。必要なのは、ネストされたオブジェクトの代わりに空のオブジェクトを書き込むことだけです。

期待される結果

 { Id:1, Field:"Value", NestedObject:{Id:1}}

私が作成しましたJsonConverter

public class SerializationConverter : JsonConverter
{
    public override bool CanRead { get { return false; } }
    public override bool CanWrite { get { return true; } }


    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Form) || typeof(Form).IsAssignableFrom(objectType);
    }

    private HashSet<Form> serializedForms = new HashSet<Form>();

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null)
            writer.WriteNull();

        var f = (Form)value;
        if (!serializedForms.Add(f))
            writer.WriteRawValue("{Id:" + f.Id.Value + "}");
        else
            serializer.Serialize(writer, value);
    }
}

しかし、予想どおり、内部呼び出しのserializer.Serialize(writer, value)シリアライザーがコンバーターを再度呼び出します。

オブジェクトが既にシリアル化されている場合にのみ、シリアル化の結果を置き換えようとしています。それ以外の場合は、デフォルトのシリアル化動作を使用します。

4

1 に答える 1

2

まず、Json.Net にはPreserveReferencesHandling、特別なコンバーターを必要とせずに、この種のことを自動的に処理できる組み込みの設定があることを述べたいと思います。にPreserveReferencesHandling設定するとAll、Json.Net は内部参照 ID を各オブジェクトに割り当て、JSON に特別なプロパティ$id$refプロパティを書き込み、参照を追跡します。あなたの例では、JSON 出力は次のようになります。

{"$id":"1","Id":1,"Field":"Value","NestedObject":{"$ref":"1"}}

これは、質問からの望ましい出力と非常によく似ていることに気付くでしょう。これには、特別なものを実装する必要なく、すべての参照を保持したまま元のオブジェクト グラフに簡単に逆シリアル化できるという利点もあります。

しかしここでは、独自の参照ループ処理を実装したい独自の理由があると仮定して、コードが機能しない理由を調べてみましょう。

Json.Net がJsonConverterオブジェクトの を検出すると、コンバーターがそのオブジェクトに必要な JSON の書き込みを処理すると想定します。したがって、特定のプロパティを含めたい場合は、それらを自分で書き出す必要があります。シリアライザーを使用してオブジェクトの一部を書き込むことができますが、オブジェクト全体をシリアライザーに渡して「これをシリアル化してください」と言うだけではいけません。

ほとんどの場合、これを行うと無限ループが発生します。あなたの場合は、への最初の呼び出しで HashSet にフォームを追加したため、そうではありませんWriteJson。シリアライザーが 2 回目のコールバックを行うと、フォームが既にセットにあるため、別の分岐が実行されます。そのため、オブジェクトの JSON 全体が、{Id:1}本当に必要なものではなくなります。

シリアライザーがコンバーターにコールバックするのを防ぐ 1 つの方法は、コンバーター内で の新しいインスタンスを作成し、メソッドJsonSerializerに渡されたインスタンスの代わりにそのインスタンスを使用することです。WriteJson新しいインスタンスにはコンバーターへの参照がないため、Form通常どおりシリアル化されます。

残念ながら、このアイデアも機能しません。内部シリアライザーにコンバーターへの参照がない場合、Json.Net がNestedObject!の特別なシリアライゼーション処理を行う方法を知る方法はありません。代わりに、エラーを回避するために に設定ReferenceLoopHandlingする必要があるため、単純に省略されます。Ignoreご覧のとおり、キャッチ 22 があります。

では、どうすればこれを機能させることができるでしょうか? さて、一歩下がって、出力に関して本当にやりたいことを再定義しましょう。

  1. 既に表示されているフォームに遭遇した場合は、Id.
  2. それ以外の場合は、これまで見てきたフォームのリストにフォームを追加してから、IdFieldおよびを出力しNestedObjectます。

どちらの場合も を出力したいIdので、ロジックを次のように単純化できることに注意してください。

  1. 常にIDを出力する
  2. まだ見たことのない Form に遭遇した場合は、見たフォームのリストにそのフォームを追加し、 and を出力しFieldますNestedObject

簡単にするために、 a を使用しJObjectて出力したいプロパティを収集writerし、最後に単にそれを に書き込むことができます。

改訂されたコードは次のとおりです。

public class SerializationConverter : JsonConverter
{
    public override bool CanRead { get { return false; } }
    public override bool CanWrite { get { return true; } }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Form) || typeof(Form).IsAssignableFrom(objectType);
    }

    private HashSet<Form> serializedForms = new HashSet<Form>();

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Form f = (Form)value;

        JObject jo = new JObject();
        jo.Add("Id", f.Id);

        if (serializedForms.Add(f))
        {
            jo.Add("Field", f.Field);
            if (f.NestedObject != null)
            {
                jo.Add("NestedObject", JToken.FromObject(f.NestedObject, serializer));
            }
        }

        jo.WriteTo(writer);
    }

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

それではテストしてみましょう:

class Program
{
    static void Main(string[] args)
    {
        Form form = new Form
        {
            Id = 1,
            Field = "Value",
        };
        form.NestedObject = form;

        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            Converters = new List<JsonConverter> { new SerializationConverter() },
            ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
        };

        string json = JsonConvert.SerializeObject(form, settings);
        Console.WriteLine(json);
    }
} 

class Form
{
    public int Id { get; set; }
    public string Field { get; set; }
    public Form NestedObject { get; set; }
}

出力は次のとおりです。

{"Id":1,"Field":"Value","NestedObject":{"Id":1}}

これまでのところ良さそうです。もっと厳密なものはどうですか:

class Program
{
    static void Main(string[] args)
    {
        List<Form> forms = new List<Form>
        {
            new Form 
            { 
                Id = 1, 
                Field = "One", 
                NestedObject = new Form
                {
                    Id = 2,
                    Field = "Two"
                }
            },
            new Form
            {
                Id = 3,
                Field = "Three"
            },
            new Form
            {
                Id = 4,
                Field = "Four"
            },
            new Form
            {
                Id = 5,
                Field = "Five"
            }
        };

        forms[0].NestedObject.NestedObject = forms[3];
        forms[1].NestedObject = forms[0].NestedObject;
        forms[2].NestedObject = forms[1];

        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            Converters = new List<JsonConverter> { new SerializationConverter() },
            ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
            Formatting = Formatting.Indented
        };

        string json = JsonConvert.SerializeObject(forms, settings);
        Console.WriteLine(json);
    }
}

出力:

[
  {
    "Id": 1,
    "Field": "One",
    "NestedObject": {
      "Id": 2,
      "Field": "Two",
      "NestedObject": {
        "Id": 5,
        "Field": "Five"
      }
    }
  },
  {
    "Id": 3,
    "Field": "Three",
    "NestedObject": {
      "Id": 2
    }
  },
  {
    "Id": 4,
    "Field": "Four",
    "NestedObject": {
      "Id": 3
    }
  },
  {
    "Id": 5
  }
]

編集

Formクラスに多数のフィールドがある場合、コンバーターでプロパティを個別にリストする代わりに、リフレクションを使用することができます。リフレクションを使用したWriteJsonメソッドは次のようになります。

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    Form f = (Form)value;

    JObject jo = new JObject();
    if (serializedForms.Add(f))
    {
        foreach (PropertyInfo prop in value.GetType().GetProperties())
        {
            if (prop.CanRead)
            {
                object propVal = prop.GetValue(value);
                if (propVal != null)
                {
                    jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
                }
            }
        }
    }
    else
    {
        jo.Add("Id", f.Id);
    }

    jo.WriteTo(writer);
}
于 2013-11-11T04:26:47.567 に答える