System.Text.Json.JsonException:サポートされていない可能性のあるオブジェクト サイクルが検出されました

System.Text.Json.JsonSerializer を使用してサイクルを持つオブジェクトをシリアル化すると、次の例外が発生します:

これは、Newtonsoft のオブジェクト サイクル例外に関するこの記事で説明したのと同じ問題ですが、この場合は Newtonsoft の代わりに System.Text.Json.JsonSerializer を使用しています。考えられる解決策は、その記事に示されているものと似ていますが、まったく同じではありません。

まず、オブジェクトサイクルとは?シリアライザーは、オブジェクトのプロパティを再帰的に処理することで機能します。すでに遭遇したオブジェクトへの参照に遭遇した場合、これは循環があることを意味します。シリアライザーはこのサイクルに対処する必要があります。そうしないと、無限に再帰し、最終的に StackOverflowException が発生します。サイクルを処理するための JsonSerializer のデフォルト戦略は、例外をスローすることです。

循環参照を持つオブジェクトの例を次に示します。 Child クラスは、Child クラスを参照する Parent クラスを参照します:

Parent harry = new Parent()
{
	Name = "Harry"
};
Parent mary = new Parent()
{
	Name = "Mary"
};
harry.Children = new List<Child>()
{
	new Child() { Name = "Barry", Dad=harry, Mom=mary }
};
mary.Children = harry.Children;

var json = JsonSerializer.Serialize(harry, new JsonSerializerOptions() 
{
	WriteIndented = true
});

Console.WriteLine(json);
Code language: C# (cs)

循環参照のため、JsonSerializer.Serialize() を呼び出すと、「オブジェクト サイクルが検出されました」JsonException がスローされます。

この記事では、この問題を解決するための 5 つの異なるオプションを紹介します。特定のシナリオに最も適したオプションを選択してください。

.NET 6 の新しいオプションを説明するために 2022 年 8 月 18 日に更新

オプション 1 – JsonIgnore 属性を使用して、シリアライザーが循環参照を持つプロパティを無視するようにする

循環参照を持つプロパティに JsonIgnore 属性を配置します。これにより、シリアライザーはこれらのプロパティのシリアライズを試行しないように指示されます。

public class Child
{
	[System.Text.Json.Serialization.JsonIgnore]
	public Parent Mom { get; set; }
	[System.Text.Json.Serialization.JsonIgnore]
	public Parent Dad { get; set; }
	public string Name { get; set; }
}
Code language: C# (cs)

結果の JSON は次のようになります:

{
	"Children": [{
		"Name": "Barry"
	}],
	"Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

この情報をシリアル化しないことを選択した場合、Mom/Dad プロパティが null であるため、反対側で逆シリアル化に問題が発生する可能性があります。

オプション 2 – 循環参照を削除する

この循環参照を誤って作成したか、プロパティが重要ではない可能性があります。どちらの場合も、解決策は簡単です:プロパティを削除してください。

例外プロパティは、この問題の一般的な原因です。この例では、Exception プロパティを持つ Message クラスがあります。

public class Message
{
	public string Name { get; set; }
	public Exception Exception { get; set; }
	public void Throw()
	{
		throw new Exception();
	}
}
Code language: C# (cs)

次に、例外をスローし、それをオブジェクトに貼り付けて、シリアル化を試みます:

try
{
	var msg = new Message()
	{
		Name = "hello world"
	};
	msg.Throw();
}
catch (Exception ex)
{
	var errorMessage = new Message()
	{
		Name = "Error",
		Exception = ex
	};

	var json = JsonSerializer.Serialize(errorMessage, new JsonSerializerOptions()
	{
		WriteIndented = true
	});

	Console.WriteLine(json);
}
Code language: C# (cs)

これにより、循環参照例外が発生します。

Exception プロパティを削除することで解決できます。代わりに、例外メッセージを保持する文字列プロパティを追加します。

public class Message
{
	public string Name { get; set; }
	public string ExceptionMessage { get; set; }
	public void Throw()
	{
		throw new Exception();
	}
}
Code language: C# (cs)

オプション 3 – 代わりに Newtonsoft を使用し、ReferenceLoopHandling.Ignore を使用します (.NET 6 より前)

.NET 6 では、循環参照を無視するためのオプションが System.Text.Json.JsonSerializer に追加されました (以下のオプション 6 を参照)。 .NET 6 より前のバージョンを使用している場合は、Newtonsoft を使用してこれを行うことができます。

まず、Newtonsoft.Json nuget パッケージを追加します。これはパッケージ マネージャー コンソールを使用しています:

 Install-Package Newtonsoft.Json
Code language: PowerShell (powershell)

次に、JsonConvert.SerializeObject() を使用して、ReferenceLoopHandling.Ignore オプションを渡します。

using Newtonsoft.Json;

var json = JsonConvert.SerializeObject(harry, Formatting.Indented,
                    new JsonSerializerSettings()
                    {
                        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                    });
Code language: C# (cs)

結果の JSON は次のようになります:

{
  "Children": [
    {
      "Mom": {
        "Name": "Mary"
      },
      "Name": "Barry"
    }
  ],
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

オプション 4 – JsonConverter を作成して、問題のあるオブジェクトのシリアライズ方法をカスタマイズする

シリアライズしているクラスを変更せずに、この循環参照の問題を解決したいとしましょう。これらは、変更できないサードパーティ クラスである場合もあります。いずれの場合でも、JsonConverter をサブクラス化し、そのオブジェクトのシリアル化を制御することで、任意のオブジェクトのシリアル化をカスタマイズできます。

まず、次のように JsonConverter サブクラスを追加します。

public class ChildJsonConverter : JsonConverter<Child>
{
	public override bool CanConvert(Type objectType)
	{
		return objectType == typeof(Child);
	}

	public override Child Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		return null; //Doesn't handle deserializing
	}

	public override void Write(Utf8JsonWriter writer, Child value, JsonSerializerOptions options)
	{
		writer.WriteStartObject();
		writer.WriteString(nameof(value.Name), value.Name);
		writer.WriteString(nameof(value.Mom), value.Mom?.Name);
		writer.WriteString(nameof(value.Dad), value.Dad?.Name);
		writer.WriteEndObject();
	}
}
Code language: C# (cs)

次に、このコンバーターを次のように JsonSerializerOptions.Converters リストに渡して使用します。

var options = new JsonSerializerOptions()
{
	WriteIndented = true
};
options.Converters.Add(new ChildJsonConverter());
var json = JsonSerializer.Serialize(harry, options);
Code language: C# (cs)

これにより、次の JSON が出力されます:

{
  "Children": [
    {
      "Name": "Barry",
      "Mom": "Mary",
      "Dad": "Harry"
    }
  ],
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

オプション 5 – オプション ReferenceHandler.Preserve を使用する (.NET 5)

.NET 5 以降、ReferenceHandler プロパティが JsonSerializerOption に追加されました。

次のように使用できます:

var json = JsonSerializer.Serialize(harry, new JsonSerializerOptions()
{
	WriteIndented = true,
	ReferenceHandler = ReferenceHandler.Preserve
});
Code language: C# (cs)

シリアル化すると、JSON にメタデータ プロパティが追加されます。したがって、次のようになります:

{
  "$id": "1",
  "Children": {
    "$id": "2",
    "$values": [
      {
        "$id": "3",
        "Mom": {
          "$id": "4",
          "Children": {
            "$ref": "2"
          },
          "Name": "Mary"
        },
        "Dad": {
          "$ref": "1"
        },
        "Name": "Barry"
      }
    ]
  },
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

この JSON にはメタデータ プロパティがあります。デシリアライザーがメタデータ プロパティの処理方法を認識している限り、問題はありません。

Newtonsoft はデフォルトでメタデータ プロパティを処理しますが、System.Text.Json では、逆シリアル化するときに ReferenceHandler プロパティを指定する必要があります。

var parent = Newtonsoft.Json.JsonConvert.DeserializeObject<Parent>(json);

var parent2 = JsonSerializer.Deserialize<Parent>(json, new JsonSerializerOptions()
{
	ReferenceHandler = ReferenceHandler.Preserve
});
Code language: C# (cs)

ここで ReferenceHandler.Preserve を指定しないと、次の例外が発生します:

このオプションを使用して循環参照を処理する場合は、デシリアライザーがメタデータ プロパティを適切に処理する方法を認識していることを確認してください。

オプション 6 – オプション ReferenceHandler.IgnoreCycles を使用 (.NET 6)

.NET 6 では、ReferenceHandler.IgnoreCycles オプションを System.Text.Json に追加しました。これにより、循環参照を無視できます。

使用方法は次のとおりです。

var json = JsonSerializer.Serialize(harry, new JsonSerializerOptions()
{
	WriteIndented = true,
	ReferenceHandler = ReferenceHandler.IgnoreCycles
});
Code language: C# (cs)

このオプションでシリアル化すると、循環参照が無効になります。これが出力するものは次のとおりです:

{
  "Children": [
    {
      "Mom": {
        "Children": null,
        "Name": "Mary"
      },
      "Dad": null,
      "Name": "Barry"
    }
  ],
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

このように null を表示したくない場合は、DefaultIgnoreCondition 設定ですべての null プロパティを無視できます:

new JsonSerializerOptions()
{
	WriteIndented = true,
	ReferenceHandler = ReferenceHandler.IgnoreCycles,
	DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
}
Code language: C# (cs)