如何使用 System.Text.Json 处理可为空的引用类型?

Posted

技术标签:

【中文标题】如何使用 System.Text.Json 处理可为空的引用类型?【英文标题】:How to deal with nullable reference types with System.Text.Json? 【发布时间】:2020-02-02 14:07:58 【问题描述】:

我已将我的项目升级到 netcore 3.0,我正在重构一个项目以使用新的可为空引用类型功能,但由于以下问题很快就卡住了。

假设我使用了一个返回以下 JSON 的 REST api:


  "Name": "Volvo 240",
  "Year": 1989

此 api 始终返回名称/年份,因此它们不可为空。

我会使用这个简单的类进行反序列化:

public class Car

    public string Name get; set;
    public int Year get; set;

我会使用新的 System.Text.Json 将其反序列化为 Car 实例

var car = JsonSerializer.Deserialize<Car>(json);

这一切都有效,但是当启用可空引用类型时,我在Car 类中收到警告,Name 被声明为不可空但可以为空。我明白为什么我会得到这个,因为可以在不初始化 Name 属性的情况下实例化这个对象。

所以理想情况下Car 应该是这样的:

public class Car

    public string Name  get; 
    public int Year  get; 

    public Car(string name, int year)
    
        Name = name;
        Year = year;
    

但这不起作用,因为System.Text.Json 序列化程序不支持带参数的构造函数。

所以我的问题是:我将如何声明 Car 以使 Name 不可为空并使其与 System.Text.Json 一起使用而不会收到“不可为空”警告?`

我不想让它可以为空,因为在启用可以为空的引用类型时,我基本上必须对所有内容进行空检查,并且由于我的示例中的 REST API 说它们总是被提供,它们不应该可以为空.

【问题讨论】:

【参考方案1】:

更新

.NET 5 的System.Text.Json 现在支持参数化构造函数,所以这应该不再是问题了。

见https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-5-0

下面的旧答案

在阅读了msdocs 之后,我发现了如何解决这个问题。

所以在System.Text.Json 无法在其构造函数中实例化带有参数的类之前,Car 类必须如下所示:

public class Car

    public string Name  get; set;  = default!;
    public int Year  get; set; 

【讨论】:

就我个人而言,我宁愿使用= "";,否则你基本上是在说(在我看来)“这个属性永远不会为空(开玩笑)” . 或者使用 JSON.NET。 System.Text.Json还没有完全替代。 .至于= default……还没有。当我在 2019 年 10 月读到这篇文章时,我想You are setting this to null!。也许明年。 这类似于微软推荐的实体对象。 EF 团队实际上建议使用null! 作为一种说法,“我知道这绝不是null。”尽管我喜欢这种可以为空的东西的潜力,但这确实感觉有点不对劲...... @MattJacobi System.Text.Json 很乐意将null 反序列化为不可为空的字符串属性;至少在默认情况下,不管反序列化类的 #nullable 状态如何(我刚刚尝试过)——是否有一些选项可以触发这种更严格的“我的意思是可空性”模式? (对于 System.Text.Json,默认情况下,通过属性省略显式 null 和隐式 null 都不是可空性错误)【参考方案2】:

更新 如果您使用net5,请使用@langen 指出的现在提供的参数化构造函数支持。下面的其他内容仍然有用。

原创 稍微另类的方法。 System.Text.Json 使用私有无参数构造函数似乎没有问题。所以你至少可以做到以下几点:

public class Car

    public string Name  get; set; 
    public int Year  get; set; 

    // For System.Text.Json deserialization only
    #pragma warning disable CS8618 // Non-nullable field is uninitialized.
    private Car()  
    #pragma warning restore CS8618

    public Car(string name, int year)
    
        Name = name
            ?? throw new ArgumentNullException(nameof(name));
        Year = year;
    

好处是:

从您自己的代码中初始化对象必须通过公共 ctor。 您无需对每个属性都执行 = null!;

使用 S.T.Json 和可为空的引用类型的缺点:

S.T.Json 仍然需要属性的设置器在反序列化期间实际设置值。我尝试使用私有对象,但不行,所以我们仍然无法获得不可变对象...

【讨论】:

仅供参考,System.Text.Json 和 Json.Net 都支持参数化构造函数,但它们不支持可为空的引用类型。使用您的示例代码,两个反序列化器都会很乐意将Name 属性设置为null,这是一个很大的问题。 空值检查并加入 ctor 总是一个好主意,因为你不能真正信任别人,好点。 在 ctor 中抛出异常会导致 ASP.net 返回 500,这不是很好。理想的行为是 ASP.net 返回一个 4XX 并告诉客户端出了什么问题。 公平地说,最初的问题并不是关于模型绑定,这总是有点痛苦。 FWIW,ASP.NET Core 6 现在考虑模型绑定/验证中的可空性并自动返回 400 Bad Request。不确定是否将 ctor 投入使用会导致 500 仍然存在。【参考方案3】:

另一种选择,对于那些想要处理具有有意义异常的缺失属性的人:

using System;

public class Car

    private string? name;
    private int? year;

    public string Name
    
        get => this.name ?? throw new InvalidOperationException($"nameof(this.Name) was not set.");
        set => this.name = value;
    

    public int Year
    
        get => this.year ?? throw new InvalidOperationException($"nameof(this.Year) was not set.");
        set => this.year = value;
    


【讨论】:

以上是关于如何使用 System.Text.Json 处理可为空的引用类型?的主要内容,如果未能解决你的问题,请参考以下文章

使用 System.Text.Json 获取嵌套属性

如何使用 System.Text.Json 忽略错误值

如何在反序列化之前使用 System.Text.Json 验证 JSON

如何在 System.Text.Json 中使用 JsonConstructor 属性

使用System.Text.Json获取嵌套属性。

如何使用 JsonConverter 在 System.Text.Json.JsonSerializer.Serialize() 中排除属性被序列化