填充已初始化的属性

从 .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)属性的值是一个包含三个元素(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 中的值一起显示在最终的反序列化结果中。 有关示例,请参阅集合属性示例

  • 对于作为具有多个属性的对象的属性,其可变属性将更新为 JSON 值,但对象引用本身不会更改。

  • 对于结构类型属性,有效行为是,对于其可变属性,保留任何现有值并添加来自 JSON 的新值。 但与引用属性不同,由于对象本身是值类型,因此不会重复使用。 相反,会修改该结构的副本,然后将其重新分配给该属性。 有关示例,请参阅结构属性示例

    结构属性必须具有资源库,否则,将在运行时引发 InvalidOperationException

注意

填充行为当前不适用于具有参数化构造函数的类型。 有关详细信息,请参阅dotnet/runtime 问题 92877

只读属性

对于填充可变的引用属性,由于属性引用的实例不会被替换,因此属性不需要有资源库。 此行为意味着反序列化还可填充只读属性

注意

结构属性仍需要资源库,因为实例将被替换为修改的副本。

集合属性示例

请考虑替换行为示例中的相同类 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]}""");

结构属性示例

以下类包含一个结构属性 S1,该属性的反序列化行为设置为 Populate。 执行此代码后,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 的值将替换为新实例。 (c.S1.Value2 仍为 5,因为 JSON 将替换默认值。)

指定方法

可通过多种方式指定替换或填充首选项

  • 使用 JsonObjectCreationHandlingAttribute 特性在类型或属性级别注释。 如果在类型级别设置属性并将其 Handling 属性设置为 Populate,则该行为将仅适用于可以填充的属性(例如,值类型必须具有资源库)。

    如果希望类型范围的首选项为 Populate,但希望从该行为中排除一个或多个属性,则可以在类型级别添加该特性,然后再次在属性级别添加以替代继承的行为。 该模式如以下代码所示。

    // 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
    };