Skip to content

Commit 4cec82a

Browse files
committed
respect nullable annotations
1 parent 8e12dee commit 4cec82a

File tree

8 files changed

+193
-30
lines changed

8 files changed

+193
-30
lines changed

docs/core/whats-new/dotnet-9/libraries.md

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -499,22 +499,11 @@ For more information, see [JSON schema exporter](../../../standard/serialization
499499

500500
<xref:System.Text.Json> now recognizes nullability annotations of properties and can be configured to enforce those during serialization and deserialization using the <xref:System.Text.Json.JsonSerializerOptions.RespectNullableAnnotations> flag.
501501

502-
The following code shows how to set the option (the `Book` type definition is shown in the previous section):
502+
The following code shows how to set the option:
503503

504504
:::code language="csharp" source="../snippets/dotnet-9/csharp/Serialization.cs" id="RespectNullable":::
505505

506-
> [!NOTE]
507-
> Due to how nullability annotations are represented in IL, the feature is restricted to annotations of non-generic properties.
508-
509-
You can also enable this setting globally using the `System.Text.Json.Serialization.RespectNullableAnnotationsDefault` feature switch in your project file (for example, _.csproj_ file):
510-
511-
```xml
512-
<ItemGroup>
513-
<RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectNullableAnnotationsDefault" Value="true" />
514-
</ItemGroup>
515-
```
516-
517-
You can configure nullability at an individual property level using the <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.IsGetNullable> and <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.IsSetNullable> properties.
506+
For more information, see [Respect nullable annotations](../../../standard/serialization/system-text-json/nullable-annotations.md).
518507

519508
### Require non-optional constructor parameters
520509

docs/core/whats-new/snippets/dotnet-9/csharp/Serialization.cs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,6 @@ public static void RunIt()
6464
// </PropertyOrder>
6565
}
6666

67-
public static void RunIt2()
68-
{
69-
// <RespectNullable>
70-
JsonSerializerOptions options = new() { RespectNullableAnnotations = true };
71-
72-
// Throws exception: System.Text.Json.JsonException: The property or field
73-
// 'Title' on type 'Serialization+Book' doesn't allow getting null values.
74-
// Consider updating its nullability annotation.
75-
JsonSerializer.Serialize(new Book { Title = null! }, options);
76-
77-
// Throws exception: System.Text.Json.JsonException: The property or field
78-
// 'Title' on type 'Serialization+Book' doesn't allow setting null values.
79-
// Consider updating its nullability annotation.
80-
JsonSerializer.Deserialize<Book>("""{ "Title" : null }""", options);
81-
// </RespectNullable>
82-
}
83-
8467
public static void RunIt3()
8568
{
8669
// <RespectRequired>
@@ -105,3 +88,30 @@ public class Book
10588
record MyPoco(string Value);
10689
// </Poco>
10790
}
91+
92+
public static class Serialization2
93+
{
94+
// <RespectNullable>
95+
public static void RunIt()
96+
{
97+
JsonSerializerOptions options = new() { RespectNullableAnnotations = true };
98+
99+
// Throws exception: System.Text.Json.JsonException: The property or field
100+
// 'Title' on type 'Serialization+Book' doesn't allow getting null values.
101+
// Consider updating its nullability annotation.
102+
JsonSerializer.Serialize(new Book { Title = null! }, options);
103+
104+
// Throws exception: System.Text.Json.JsonException: The property or field
105+
// 'Title' on type 'Serialization+Book' doesn't allow setting null values.
106+
// Consider updating its nullability annotation.
107+
JsonSerializer.Deserialize<Book>("""{ "Title" : null }""", options);
108+
}
109+
110+
public class Book
111+
{
112+
public required string Title { get; set; }
113+
public string? Author { get; set; }
114+
public int PublishYear { get; set; }
115+
}
116+
// </RespectNullable>
117+
}

docs/fundamentals/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,8 @@ items:
737737
href: ../standard/serialization/system-text-json/deserialization.md
738738
- name: Require JSON properties
739739
href: ../standard/serialization/system-text-json/required-properties.md
740+
- name: Respect nullable annotations
741+
href: ../standard/serialization/system-text-json/nullable-annotations.md
740742
- name: Allow invalid JSON
741743
href: ../standard/serialization/system-text-json/invalid-json.md
742744
- name: Handle missing members
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
title: Respect nullable annotations
3+
description: "Learn how to configure serialization and deserialization to respect nullable annotations."
4+
ms.date: 10/22/2024
5+
no-loc: [System.Text.Json, Newtonsoft.Json]
6+
---
7+
# Respect nullable annotations
8+
9+
Starting in .NET 9, <xref:System.Text.Json.JsonSerializer> has (limited) support for non-nullable reference type enforcement in both serialization and deserialization. You can toggle this support using the <xref:System.Text.Json.JsonSerializerOptions.RespectNullableAnnotations?displayProperty=nameWithType> flag.
10+
11+
For example, the following code snippet throws a <xref:System.Text.Json.JsonException> during serialization with a message like:
12+
13+
> The property or field 'Name' on type 'Person' doesn't allow getting null values. Consider updating its nullability annotation.
14+
15+
:::code language="csharp" source="snippets/nullable-annotations/Nullable.cs" id="Serialization":::
16+
17+
Similarly, <xref:System.Text.Json.JsonSerializerOptions.RespectNullableAnnotations> enforces nullability on deserialization. The following code snippet throws a <xref:System.Text.Json.JsonException> during serialization with a message like:
18+
19+
> The constructor parameter 'Name' on type 'Person' doesn't allow null values. Consider updating its nullability annotation.
20+
21+
:::code language="csharp" source="snippets/nullable-annotations/Nullable.cs" id="Deserialization":::
22+
23+
> [!TIP]
24+
> You can configure nullability at an individual property level using the <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.IsGetNullable> and <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.IsSetNullable> properties.
25+
26+
## Limitations
27+
28+
Due to how non-nullable reference types are implemented, this feature comes with some important limitations. Familiarize yourself with these limitations before turning the feature on. The root of the issue is that reference type nullability has no first-class representation in intermediate language (IL). As such, the expressions `MyPoco` and `MyPoco?` are indistinguishable from the perspective of run-time reflection. While the compiler will try to make up for that by [emitting attribute metadata where possible](https://sharplab.io/#v2:D4AQTAjAsAULBOBTAxge3gEwAQFkCeACqmgBQgQAMWAcqgC7UCuANswMp3wCWAdgOYAaLOQoB+Gi2YBDAEbNEHbvwCUAbiA=), this metadata is restricted to non-generic member annotations that are scoped to a particular type definition. This is the reason that the flag only validates nullability annotations that are present on non-generic properties, fields, and constructor parameters. System.Text.Json does not support nullability enforcement on:
29+
30+
- Top-level types, or the type that's passed when making the first `JsonSerializer.(De)serialize` call.
31+
- Collection element types&mdash;the `List<string>` and `List<string?>` types are indistinguishable.
32+
- Any properties, fields, or constructor parameters that are generic.
33+
34+
If you want to add nullability enforcement in these cases, either model your type to be a struct (since they don't admit null values), or author a custom converter that overrides its <xref:System.Text.Json.Serialization.JsonConverter`1.HandleNull> property to `true`.
35+
36+
## Feature switch
37+
38+
You can turn on the `RespectNullableAnnotations` setting globally using the `System.Text.Json.Serialization.RespectNullableAnnotationsDefault` feature switch. Add the following MSBuild item to your project file (for example, *.csproj* file):
39+
40+
```xml
41+
<ItemGroup>
42+
<RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectNullableAnnotationsDefault" Value="true" />
43+
</ItemGroup>
44+
```
45+
46+
The `RespectNullableAnnotationsDefault` API was implemented as an opt-in flag in .NET 9 to avoid breaking existing applications. If you're writing a new application, it's highly recommended that you enable this flag in your code.
47+
48+
## Relationship between nullable and optional parameters
49+
50+
<xref:System.Text.Json.JsonSerializerOptions.RespectNullableAnnotations> doesn't extend enforcement to unspecified JSON values, because System.Text.Json treats required and non-nullable properties as orthogonal concepts. For example, the following code snippet doesn't throw an exception during deserialization:
51+
52+
:::code language="csharp" source="snippets/nullable-annotations/Nullable.cs" id="Unspecified":::
53+
54+
This behavior stems from the C# language itself, where you can have required properties that are nullable:
55+
56+
```csharp
57+
MyPoco poco = new() { Value = null }; // No compiler warnings.
58+
59+
class MyPoco
60+
{
61+
public required string? Value { get; set; }
62+
}
63+
```
64+
65+
And you can also have optional properties that are non-nullable:
66+
67+
```csharp
68+
class MyPoco
69+
{
70+
public string Value { get; set; } = "default";
71+
}
72+
```
73+
74+
The same orthogonality applies to constructor parameters:
75+
76+
```csharp
77+
record MyPoco(
78+
string RequiredNonNullable,
79+
string? RequiredNullable,
80+
string OptionalNonNullable = "default",
81+
string? OptionalNullable = "default");
82+
```
83+
84+
## See also
85+
86+
- [Non-optional constructor parameters](required-properties.md#non-optional-constructor-parameters)

docs/standard/serialization/system-text-json/required-properties.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,7 @@ You can turn on the `RespectRequiredConstructorParameters` setting globally usin
6060
```
6161

6262
The `RespectRequiredConstructorParametersDefault` API was implemented as an opt-in flag in .NET 9 to avoid breaking existing applications. If you're writing a new application, it's highly recommended that you enable this flag in your code.
63+
64+
## See also
65+
66+
- [Respect nullable annotations](nullable-annotations.md)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Text.Json;
2+
3+
public static class Nullable1
4+
{
5+
// <Unspecified>
6+
public static void RunIt()
7+
{
8+
JsonSerializerOptions options = new()
9+
{
10+
RespectNullableAnnotations = true
11+
};
12+
var result = JsonSerializer.Deserialize<MyPoco>("{}", options);
13+
Console.WriteLine(result.Name is null); // True.
14+
}
15+
16+
class MyPoco
17+
{
18+
public string Name { get; set; }
19+
}
20+
// </Unspecified>
21+
}
22+
23+
public static class Nullable2
24+
{
25+
// <Serialization>
26+
public static void RunIt()
27+
{
28+
#nullable enable
29+
JsonSerializerOptions options = new()
30+
{
31+
RespectNullableAnnotations = true
32+
};
33+
34+
Person invalidValue = new(Name: null!);
35+
JsonSerializer.Serialize(invalidValue, options);
36+
}
37+
38+
record Person(string Name);
39+
// </Serialization>
40+
}
41+
42+
public static class Nullable3
43+
{
44+
// <Deserialization>
45+
public static void RunIt()
46+
{
47+
#nullable enable
48+
JsonSerializerOptions options = new()
49+
{
50+
RespectNullableAnnotations = true
51+
};
52+
53+
string json = """{"Name":null}""";
54+
JsonSerializer.Deserialize<Person>(json, options);
55+
}
56+
57+
record Person(string Name);
58+
// </Deserialization>
59+
}
60+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//Nullable1.RunIt();
2+
//Nullable2.RunIt();
3+
Nullable3.RunIt();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
</PropertyGroup>
9+
</Project>

0 commit comments

Comments
 (0)