System.Text.Jsonで共用体型をシリアル化する

.NET 11 以降、JsonSerializer は C# 15 の union 型をサポートします。 共用体は、その宣言内でケース型の 1 つを保持しています。 JsonSerializer はアクティブなケース値を書き込み、読み戻すことができます。

共用体値のシリアル化と逆シリアル化

各ケースが異なる JSON トークン型を持つ共用体を宣言します。

public union Payload(int, string, Message);
public sealed record Message(string Text);

JsonSerializerを使用して、共用体をシリアル化および逆シリアル化します。

Payload payload = new Message("Ready");
string json = JsonSerializer.Serialize(payload);
Payload copy = JsonSerializer.Deserialize<Payload>(json);

シリアル化された JSON には、ラッパーや判別子ではなく、有効なケースの値が含まれます。

{"Text":"Ready"}

既定では、シリアライザーは受信 JSON をトークンの種類別に分類します。 上記の共用体では、JSON 番号が int選択され、JSON 文字列によって stringが選択され、JSON オブジェクトによって Messageが選択されます。

JsonSerializerOptions.Webでは、JSON 文字列からintを読み取ることもできます。 Payloadのintとstringの両方のケースで、文字列トークンの種類を要求します。 シリアライザーがいずれかのケースを解析する前に、"25%" も曖昧さのために JsonException をスローします。 Web の既定値を使用して文字列を読み取る場合は、ケースを選択する カスタム分類子を指定 します。

同じ JSON トークン型のケースを区別する

トークン分類では、両方とも JSON オブジェクトとしてシリアル化される 2 つのケースを区別できません。 JsonUnionAttribute適用し、JsonUnionTypeStructuralClassifierを選択して、プロパティ名でオブジェクト ケースを分類します。

[JsonUnion(TypeClassifier = typeof(JsonUnionTypeStructuralClassifier))]
public union Pet(Dog, Cat);
public sealed record Dog(string Name, string Breed);
public sealed record Cat(string Name, int Lives);

ここでは、 JsonUnionAttribute 既存の Pet 共用体の分類子を選択します。通常の型に適用しても、その型は共用体になりません。

分類子は、ペイロードにBreedが含まれている場合にDogを選択し、Livesが含まれるときにCatを選択します。

Pet pet = JsonSerializer.Deserialize<Pet>(
    """{"Name":"Rex","Breed":"Husky"}""");

JSON オブジェクトの場合、構造分類器は互換性のあるオブジェクトの候補から始め、認識されたルートレベルのプロパティ名を読み取るにつれて、その候補集合を絞り込みます。 必須プロパティは、それらが存在しないケースを除外します。 JsonUnmappedMemberHandling.Disallow は、ケースが宣言しないプロパティがペイロードに含まれている場合にケースを削除します。 分類は、1 つのケースが残っている場合にのみ成功します。

構造分類子は、プロパティ値、入れ子になったオブジェクト、文字列の内容、または配列要素を検査しません。 次の結果に留意してください。

  • 0 個または複数の候補を残すペイロードは、 JsonExceptionをスローします。
  • 分類子の作成時に、重なり合うオブジェクト コントラクトまたはシャドウオブジェクト コントラクトが拒否される場合があります。
  • 同じ JSON トークン型を使用する複数の非オブジェクト ケースはサポートされていません。 たとえば、 Guid と string の両方で JSON 文字列が使用されます。
  • プレーン オブジェクトのケースは、ディクショナリ、JsonObject、または他の POCO ではないオブジェクト形状のケースと混在させることはできません。
  • 入れ子になった共用体のケースとポリモーフィックなケースはサポートされていません。
  • ReferenceHandler.Preserveはサポートされていません。
  • 大文字と小文字を区別できない構成の場合、シリアライザーが分類子を構築する際に NotSupportedException がスローされます。

共用体または閉じた階層を選択する

制御しない識別子のない JSON 形式を保持する必要がある場合、またはケースに個別の JSON 図形がある場合は、共用体を使用します。 たとえば、Payloadのintとstringのケースは、JSON トークンの種類によって区別できます。 ただし、 Pet(Dog, Cat)などのオブジェクト ケースの場合、プロパティ名の変更は、構造分類子が選択するケースに影響を与える可能性があります。

型と JSON コントラクトを制御すると、 推論されたポリモーフィズムを持つ閉じた階層で 、代わりに識別子を使用してオブジェクト ケースを識別できます。

[JsonPolymorphic(InferClosedTypePolymorphism = true)]
public closed record Event;
public sealed record Created(int Id) : Event;
public sealed record Deleted(int Id) : Event;

JsonSerializer.Serialize<Event>(new Created(42)) は {"$type":"Created","Id":42}を書き込みます。 どちらの派生型も Idを宣言しますが、識別子はそのプロパティとは別にケースを識別します。 これにより、プロパティが進化するにつれて、ケースの選択がより安定します。 共用体のケースとは異なり、派生型は基底クラスを共有する必要があり、推論されたポリモーフィズムをオプトインする必要があります。 closed 修飾子だけでは、識別子は追加されません。

カスタム分類子を提供する

既定のトークン分類または組み込みのJsonUnionTypeStructuralClassifierが要件を満たしていない場合に、JsonTypeClassifierFactoryから派生します。 カスタム分類器は、ほかの構造上のルールを使ってユニオンのケースを選択できます。 次のいずれかの場所にファクトリを登録します。

分類子は現在の JSON 値を読み取り、 JsonTypeClassifierContext.UnionCasesからケース型のいずれかを返します。 シリアライザーは、最初にコントラクト デリゲートをチェックし、次に共用体ごとのファクトリ、オプション レベルのファクトリ、組み込みのトークン分類を確認します。

あいまいな共用体の場合、生成時に分類子が構成されていない限り、ソース ジェネレーターは診断を報告します。

null 値と既定の共用体値を処理する

共用体では、Null 許容のケース型を宣言することができます。 JSON null は、最初の null 許容ケースを選択します。 共用体に null 許容ケースがない場合、JSON null は既定の共用体値を生成します。 コンパイラによって生成された構造体共用体の場合、既定値にはアクティブなケースがなく、JSON nullとしてシリアル化されます。

ユニオン型のコントラクトをカスタマイズする

高度なシナリオでは、 JsonTypeInfoを使用して共用体メタデータをカスタマイズします。 共用体の JsonTypeInfo.Kind の値は JsonTypeInfoKind.Union です。 そのコントラクトは、次の内容を公開します。

JsonTypeInfoの変更の詳細については、「JSON コントラクトのカスタマイズ」を参照してください。

こちらも参照ください