初期化されたプロパティを設定する

.NET 8 以降では、JSON が逆シリアル化されるときに、.NET プロパティを置き換える設定するように基本設定を指定できます。 JsonObjectCreationHandling 列挙型では、オブジェクト作成処理の選択肢が提供されます。

既定の (置換) 動作

System.Text.Json デシリアライザーでは、常にターゲット型の新しいインスタンスが作成されます。 ただし、新しいインスタンスが作成された場合でも、一部のプロパティとフィールドはオブジェクトの構築の一部として既に初期化されている可能性があります。 次の 型があるとします:

class A
{
    public List<int> Numbers1 { get; } = [1, 2, 3];
    public List<int> Numbers2 { get; set; } = [1, 2, 3];
}

このクラスのインスタンスを作成すると、Numbers1 (および Numbers2) プロパティの値は 3 つの要素 (1、2、3) を含むリストになります。 JSON をこの型に逆シリアル化する場合、''既定'' の動作では、プロパティ値が ''置き換え'' られます。

  • Numbers1の場合、読み取り専用 (セッターなし) であるため、値 1、2、および 3 は引き続きリストに含まれます。
  • Numbers2(読み取り/書き込み) の場合、新しいリストが割り当てられ、JSON の値が追加されます。

たとえば、次の逆シリアル化コードを実行する場合、Numbers1 には値 1、2、3 が含まれ、Numbers2 には値 4、5、6 が含まれます。

A? a = JsonSerializer.Deserialize<A>("""{"Numbers1": [4,5,6], "Numbers2": [4,5,6]}""");

設定の動作

.NET 8 以降では、逆シリアル化の動作を変更して、プロパティとフィールドを置き換えるのではなく、変更 (''設定'') できます。

  • コレクション型プロパティの場合、オブジェクトはクリアされずに再利用されます。 コレクションに要素が事前に設定されている場合、JSON からの値と共に、最終的な逆シリアル化された結果に表示されます。 例については、「コレクション プロパティの例」を参照してください。

  • 複数のプロパティを含む 1 つのオブジェクトであるプロパティの場合、その変更可能なプロパティは JSON 値に更新されますが、オブジェクト参照自体は変更されません。

  • 構造体型プロパティの場合、効果的な動作では、その変更可能なプロパティに対して、既存の値が保持され、JSON からの新しい値が追加されます。 しかし、参照プロパティとは異なり、オブジェクト自体は値型であるため再利用されません。 代わりに、構造体のコピーが変更され、プロパティに再割り当てされます。 例については、「構造体プロパティの例」を参照してください。

    構造体プロパティにはセッターが必要です。これがないと、実行時に InvalidOperationException がスローされます。

Note

現在、パラメーター化されたコンストラクターを持つ型に対しては、データの設定動作が機能しません。 詳細については、dotnet/runtime issue 92877 を参照してください。

読み取り専用プロパティ

変更可能な参照プロパティを設定する場合、プロパティで参照されるインスタンスは ''置き換え'' られないため、プロパティにはセッターは必要ありません。 この動作は、逆シリアル化で ''読み取り専用'' のプロパティも設定できることを意味します。

Note

インスタンスが変更されたコピーに置き換えられるため、構造体プロパティには引き続きセッターが必要です。

コレクション プロパティの例

置換動作の例と同じクラス A について考えますが、今回はプロパティを置き換えるのではなく、''設定する'' ための基本設定で注釈を付けます。

[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
class A
{
    public List<int> Numbers1 { get; } = [1, 2, 3];
    public List<int> Numbers2 { get; set; } = [1, 2, 3];
}

次の逆シリアル化コードを実行すると、Numbers1Numbers2 の両方に値 1、2、3、4、5、6 が含まれます。

A? a = JsonSerializer.Deserialize<A>("""{"Numbers1": [4,5,6], "Numbers2": [4,5,6]}""");

構造体プロパティの例

次のクラスには、逆シリアル化動作が Populate に設定されている構造体プロパティ S1 が含まれています。 このコードを実行した後、c.S1.Value1 の値は 10 (コンストラクターから) になり、c.S1.Value2 の値は 5 (JSON から) になります。

C? c = JsonSerializer.Deserialize<C>("""{"S1": {"Value2": 5}}""");

class C
{
    public C()
    {
        _s1 = new S
        {
            Value1 = 10
        };
    }

    private S _s1;

    [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
    public S S1
    {
        get { return _s1; }
        set { _s1 = value; }
    }
}

struct S
{
    public int Value1 { get; set; }
    public int Value2 { get; set; }
}

代わりに既定の Replace 動作が使用された場合、c.S1.Value1 は逆シリアル化後に既定値の 0 になります。 これは、コンストラクター C() が呼び出され、c.S1.Value1 が 10 に設定されていても、S1 の値が新しいインスタンスに置き換えられるためです (JSON では既定値が置き換わるため、c.S1.Value2 は 5 のままです)。

指定方法

置換または設定の基本設定を指定するには、複数の方法があります。

  • JsonObjectCreationHandlingAttribute 属性を使用して、型またはプロパティ レベルで注釈を付けます。 属性を型レベルで設定し、その Handling プロパティを Populate に設定した場合、動作は、設定が可能なプロパティにのみ適用されます (たとえば、値型にはセッターが必要です)。

    型全体の基本設定は Populate にするものの、その動作から 1 つまたは複数のプロパティを除外する場合は、型レベルで属性を追加し、プロパティ レベルでもう一度属性を追加して、継承された動作をオーバーライドできます。 そのパターンを次のコードに示します。

    // Type-level preference is Populate.
    [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
    class B
    {
        // For this property only, use Replace behavior.
        [JsonObjectCreationHandling(JsonObjectCreationHandling.Replace)]
        public List<int> Numbers1 { get; } = [1, 2, 3];
        public List<int> Numbers2 { get; set; } = [1, 2, 3];
    }
    
  • グローバル設定を指定するには、JsonSerializerOptions.PreferredObjectCreationHandling (またはソース生成の場合は JsonSourceGenerationOptionsAttribute.PreferredObjectCreationHandling) を設定します。

    var options = new JsonSerializerOptions
    {
        PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate
    };