Compartilhar via


Respeitar anotações anuláveis

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

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

A propriedade ou campo "Name" no tipo "Person" não permite obter valores nulos. Considere atualizar sua anotação de nulidade.

    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, o RespectNullableAnnotations impõe nulidade na desserialização. O seguinte trecho de código lança um JsonException durante a serialização com uma mensagem como:

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

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

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

    record Person(string Name);

Dica

Limitações

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

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

Se você quiser adicionar a imposição de nulidade 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 propriedade HandleNull para true.

Opção de recurso

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

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

A API RespectNullableAnnotationsDefault 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 habilitar esse sinalizador no seu código.

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

O RespectNullableAnnotations não estende a imposição a valores JSON não especificados, pois 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 decorre da própria linguagem C#, onde você pode ter as propriedades necessárias que podem ser anuladas:

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 permitem valor nulo:

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

A mesma ortogonalidade é aplicada aos parâmetros do construtor:

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

Valores ausentes versus valores nulos

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

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

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

  • Uma propriedade ausente não gera uma exceção, mesmo para propriedades não anuláveis. Por exemplo, {} não gera uma exceção ao desserializar para uma propriedade não anulável string Name . O serializador não define a propriedade, deixando-a no valor padrão do construtor. Para um tipo de referência não anulável não inicializado, isso resulta em null, o que aciona 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.

Confira também