まず、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 があります。
では、どうすればこれを機能させることができるでしょうか? さて、一歩下がって、出力に関して本当にやりたいことを再定義しましょう。
- 既に表示されているフォームに遭遇した場合は、
Id
.
- それ以外の場合は、これまで見てきたフォームのリストにフォームを追加してから、
Id
、Field
およびを出力しNestedObject
ます。
どちらの場合も を出力したいId
ので、ロジックを次のように単純化できることに注意してください。
- 常にIDを出力する
- まだ見たことのない 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);
}