Partilhar via


Respeitar anotações anuláveis

A partir do .NET 9, JsonSerializer tem suporte (limitado) para imposição de tipo de referência não anulável na serialização e desserialização. Você pode alternar esse suporte usando o JsonSerializerOptions.RespectNullableAnnotations sinalizador.

Por exemplo, o trecho de código a seguir lança um JsonException durante a serialização com uma mensagem como:

A propriedade ou campo 'Nome' no tipo 'Pessoa' não permite obter valores nulos. Considere atualizar sua anotação de anulabilidade.

    public static void RunIt()
    {
#nullable enable
        JsonSerializerOptions options = new()
        {
            RespectNullableAnnotations = true
        };

        Person invalidValue = new(Name: null!);
        JsonSerializer.Serialize(invalidValue, options);
    }

    record Person(string Name);

Da mesma forma, RespectNullableAnnotations impõe a anulabilidade na desserialização. O trecho de código a seguir lança um JsonException durante a serialização com uma mensagem como:

O parâmetro do construtor 'Name' no tipo 'Person' não permite valores nulos. Considere atualizar sua anotação de anulabilidade.

    public static void RunIt()
    {
#nullable enable
        JsonSerializerOptions options = new()
        {
            RespectNullableAnnotations = true
        };

        string json = """{"Name":null}""";
        JsonSerializer.Deserialize<Person>(json, options);
    }

    record Person(string Name);

Gorjeta

Limitações

Devido à forma como os tipos de referência não anuláveis são implementados, este recurso vem com algumas limitações importantes. Familiarize-se com essas limitações antes de ativar o recurso. A raiz do problema é que a anulabilidade do tipo de referência não tem representação de primeira classe em linguagem intermediária (IL). Assim, as expressões MyPoco e MyPoco? são indistinguíveis da perspetiva da reflexão em tempo de execução. Enquanto o compilador tenta compensar isso emitindo metadados de atributo (consulte sharplab.io exemplo), esses metadados são restritos a anotações de membros não genéricos que têm escopo para uma definição de tipo específica. Essa limitação é a razão pela qual o sinalizador só valida anotações de anulabilidade que estão presentes em propriedades não genéricas, campos e parâmetros do construtor. System.Text.Json não suporta a aplicação de anulabilidade em:

  • Tipos de nível superior ou o tipo que é passado ao fazer a primeira JsonSerializer.Deserialize() ou JsonSerializer.Serialize() chamada.
  • Tipos de elementos de coleção — por exemplo, os List<string> tipos e List<string?> são indistinguíveis.
  • Quaisquer propriedades, campos ou parâmetros do construtor que sejam genéricos.

Se você quiser adicionar imposição de anulabilidade nesses casos, modele seu tipo para ser um struct (já que eles não admitem valores nulos) ou crie um conversor personalizado que substitua sua HandleNull propriedade para true.

Mudança de funcionalidade

Você pode ativar a RespectNullableAnnotations configuração globalmente usando a System.Text.Json.Serialization.RespectNullableAnnotationsDefault opção de recurso. Adicione o seguinte item MSBuild ao seu arquivo de projeto (por exemplo, arquivo .csproj ):

<ItemGroup>
  <RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectNullableAnnotationsDefault" Value="true" />
</ItemGroup>

A RespectNullableAnnotationsDefault API foi implementada como um sinalizador de aceitação no .NET 9 para evitar a interrupção de aplicativos existentes. Se você estiver escrevendo um novo aplicativo, é altamente recomendável que você habilite esse sinalizador em seu código.

Relação entre parâmetros anuláveis e opcionais

RespectNullableAnnotations não estende a imposição a valores JSON não especificados, porque System.Text.Json trata as propriedades necessárias e não anuláveis como conceitos ortogonais. Por exemplo, o trecho de código a seguir não gera uma exceção durante a desserialização:

public static void RunIt()
{
    JsonSerializerOptions options = new()
    {
        RespectNullableAnnotations = true
    };
    var result = JsonSerializer.Deserialize<MyPoco>("{}", options);
    Console.WriteLine(result.Name is null); // True.
}

class MyPoco
{
    public string Name { get; set; }
}

Esse comportamento deriva da própria linguagem C#, onde você pode ter propriedades necessárias que são anuláveis:

MyPoco poco = new() { Value = null }; // No compiler warnings.

class MyPoco
{
    public required string? Value { get; set; }
}

E você também pode ter propriedades opcionais que não são anuláveis:

class MyPoco
{
    public string Value { get; set; } = "default";
}

A mesma ortogonalidade aplica-se aos parâmetros do construtor:

record MyPoco(
    string RequiredNonNullable,
    string? RequiredNullable,
    string OptionalNonNullable = "default",
    string? OptionalNullable = "default"
    );

Valores em falta versus valores nulos

É importante entender a distinção entre propriedades JSON ausentes e propriedades com valores explícitos null quando você define RespectNullableAnnotations. JavaScript distingue entre undefined (propriedade ausente) e null (valor nulo explícito). No entanto, o .NET não tem um undefined conceito, portanto, ambos os casos são desserializados para null no .NET.

Durante a desserialização, quando RespectNullableAnnotations é true:

  • Um valor nulo explícito lança uma exceção para propriedades não anuláveis. Por exemplo, {"Name":null} lança uma exceção ao desserializar para uma propriedade não anulável string Name .

  • Uma propriedade ausente não gera uma exceção, mesmo para propriedades não anuláveis. Por exemplo, {} não lança uma exceção quando desserializa para uma propriedade não anulável string Name. O serializador não define a propriedade, deixando-a em seu valor padrão do construtor. Para um tipo de referência não anulável não inicializado, isso resulta em null, que dispara um aviso do compilador.

    O código a seguir mostra como uma propriedade ausente NÃO gera uma exceção durante a desserialização:

        public static void RunIt()
        {
    #nullable enable
            JsonSerializerOptions options = new()
            {
                RespectNullableAnnotations = true
            };
    
            // Missing property - does NOT throw an exception.
            string jsonMissing = """{}""";
            var resultMissing = JsonSerializer.Deserialize<Person>(jsonMissing, options);
            Console.WriteLine(resultMissing.Name is null); // True.
        }
    
        record Person(string Name);
    

Essa diferença de comportamento ocorre porque as propriedades ausentes são tratadas como opcionais (não fornecidas), enquanto os valores explícitos null são tratados como valores fornecidos que violam a restrição não anulável. Se você precisar impor que uma propriedade deve estar presente no JSON, use o required modificador ou configure a propriedade conforme necessário usando JsonRequiredAttribute ou o modelo de contratos.

Consulte também