Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0221](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0221.md)|Design|TryGetValue method should use \[MaybeNullWhen(false)\] on the value parameter|ℹ️|❌|✔️|❌|
|[MA0222](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0222.md)|Design|JsonSourceGenerationOptions should set RespectNullableAnnotations|⚠️|❌|✔️|❌|
|[MA0223](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0223.md)|Design|JsonSourceGenerationOptions should set RespectRequiredConstructorParameters|⚠️|❌|✔️|❌|
|[MA0224](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0224.md)|Design|JsonSerializerOptions should set RespectNullableAnnotations|⚠️|❌|✔️|❌|
|[MA0225](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0225.md)|Design|JsonSerializerOptions should set RespectRequiredConstructorParameters|⚠️|❌|✔️|❌|

<!-- rules -->

Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@
|[MA0221](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0221.md)|Design|TryGetValue method should use \[MaybeNullWhen(false)\] on the value parameter|<span title='Info'>ℹ️</span>|❌|✔️|❌|
|[MA0222](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0222.md)|Design|JsonSourceGenerationOptions should set RespectNullableAnnotations|<span title='Warning'>⚠️</span>|❌|✔️|❌|
|[MA0223](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0223.md)|Design|JsonSourceGenerationOptions should set RespectRequiredConstructorParameters|<span title='Warning'>⚠️</span>|❌|✔️|❌|
|[MA0224](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0224.md)|Design|JsonSerializerOptions should set RespectNullableAnnotations|<span title='Warning'>⚠️</span>|❌|✔️|❌|
|[MA0225](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0225.md)|Design|JsonSerializerOptions should set RespectRequiredConstructorParameters|<span title='Warning'>⚠️</span>|❌|✔️|❌|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down
50 changes: 50 additions & 0 deletions docs/Rules/MA0224.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# MA0224 - JsonSerializerOptions should set RespectNullableAnnotations
<!-- sources -->
Sources: [JsonSerializerOptionsAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/JsonSerializerOptionsAnalyzer.cs), [JsonSerializerOptionsFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/JsonSerializerOptionsFixer.cs)
<!-- sources -->

`System.Text.Json` does not enforce the nullable annotations of the types it serializes and deserializes. `RespectNullableAnnotations`, introduced in .NET 9, makes the serializer throw when a `null` value is read into, or written from, a non-nullable property, field or constructor parameter. It defaults to `false` for backward compatibility, so a `JsonSerializerOptions` instance that does not configure it silently produces instances whose non-nullable members are `null`.

This rule reports the creation of a `JsonSerializerOptions` that does not configure the option. It does not require a particular value: setting the option to `false` is a deliberate choice, and satisfies the rule as much as setting it to `true`.

[MA0225](MA0225.md) reports the closely related `RespectRequiredConstructorParameters` option. [MA0222](MA0222.md) reports the same option on the `[JsonSourceGenerationOptions]` attribute of a `JsonSerializerContext`.

````csharp
using System.Text.Json;

// ❌ The option is not configured, and deserializing {"Name":null} sets Name to null
var options = new JsonSerializerOptions();

// ✅ Deserializing {"Name":null} throws
var options = new JsonSerializerOptions { RespectNullableAnnotations = true };

// ✅ The legacy behavior is kept, but the choice is explicit
var options = new JsonSerializerOptions { RespectNullableAnnotations = false };

// ✅ The option is set on the local the creation is assigned to
var options = new JsonSerializerOptions();
options.RespectNullableAnnotations = true;

// ✅ JsonSerializerDefaults.Strict, introduced in .NET 10, sets the option
var options = new JsonSerializerOptions(JsonSerializerDefaults.Strict);
````

Note that the option only rejects the `null` values that are present in the payload: a missing property is left to its default value, which is `null` for an uninitialized non-nullable reference type. Use the `required` modifier or `[JsonRequired]` to reject the payloads that do not contain the property.

The rule does not report anything when the target framework does not support the option.

The rule only reports the creation of a `JsonSerializerOptions`, and considers the option configured when it is set in the object initializer, or on the local the creation is assigned to in the same method. It does not report the options that are configured without being created, such as the ones of `ConfigureHttpJsonOptions` or `AddJsonOptions` in an ASP.NET Core application, and it does not report the copy constructor, as the copied instance can be configured somewhere else.

The option can also be enabled for the whole application with the `System.Text.Json.Serialization.RespectNullableAnnotationsDefault` feature switch, which this rule does not detect:

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

This rule is disabled by default as it requires every `JsonSerializerOptions` to configure the option. To enable it, add the following to your `.editorconfig` file:

````editorconfig
dotnet_diagnostic.MA0224.severity = warning
````
50 changes: 50 additions & 0 deletions docs/Rules/MA0225.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# MA0225 - JsonSerializerOptions should set RespectRequiredConstructorParameters
<!-- sources -->
Sources: [JsonSerializerOptionsAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/JsonSerializerOptionsAnalyzer.cs), [JsonSerializerOptionsFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/JsonSerializerOptionsFixer.cs)
<!-- sources -->

For historical reasons, `System.Text.Json` treats all the constructor parameters as optional, and fills the ones that are missing from the payload with their default value. `RespectRequiredConstructorParameters`, introduced in .NET 9, makes the serializer throw when a non-optional constructor parameter is missing. It defaults to `false` for backward compatibility, so a `JsonSerializerOptions` instance that does not configure it silently produces instances built from an incomplete payload.

This rule reports the creation of a `JsonSerializerOptions` that does not configure the option. It does not require a particular value: setting the option to `false` is a deliberate choice, and satisfies the rule as much as setting it to `true`.

[MA0224](MA0224.md) reports the closely related `RespectNullableAnnotations` option. [MA0223](MA0223.md) reports the same option on the `[JsonSourceGenerationOptions]` attribute of a `JsonSerializerContext`.

````csharp
using System.Text.Json;

// ❌ The option is not configured, and deserializing {} creates a Person whose Id is 0
var options = new JsonSerializerOptions();

// ✅ Deserializing {} throws as Id is missing
var options = new JsonSerializerOptions { RespectRequiredConstructorParameters = true };

// ✅ The legacy behavior is kept, but the choice is explicit
var options = new JsonSerializerOptions { RespectRequiredConstructorParameters = false };

// ✅ The option is set on the local the creation is assigned to
var options = new JsonSerializerOptions();
options.RespectRequiredConstructorParameters = true;

// ✅ JsonSerializerDefaults.Strict, introduced in .NET 10, sets the option
var options = new JsonSerializerOptions(JsonSerializerDefaults.Strict);

record Person(int Id);
````

The rule does not report anything when the target framework does not support the option.

The rule only reports the creation of a `JsonSerializerOptions`, and considers the option configured when it is set in the object initializer, or on the local the creation is assigned to in the same method. It does not report the options that are configured without being created, such as the ones of `ConfigureHttpJsonOptions` or `AddJsonOptions` in an ASP.NET Core application, and it does not report the copy constructor, as the copied instance can be configured somewhere else.

The option can also be enabled for the whole application with the `System.Text.Json.Serialization.RespectRequiredConstructorParametersDefault` feature switch, which this rule does not detect:

````xml
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectRequiredConstructorParametersDefault" Value="true" />
</ItemGroup>
````

This rule is disabled by default as it requires every `JsonSerializerOptions` to configure the option. To enable it, add the following to your `.editorconfig` file:

````editorconfig
dotnet_diagnostic.MA0225.severity = warning
````
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Microsoft.CodeAnalysis.Formatting;

namespace Meziantou.Analyzer.Rules;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class JsonSerializerOptionsFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.SetRespectNullableAnnotationsOnJsonSerializerOptions, RuleIdentifiers.SetRespectRequiredConstructorParametersOnJsonSerializerOptions);

public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root?.FindNode(context.Span, getInnermostNodeForTie: true) is not BaseObjectCreationExpressionSyntax creation)
return;

foreach (var diagnostic in context.Diagnostics)
{
var propertyName = GetPropertyName(diagnostic.Id);
if (propertyName is null)
continue;

var title = $"Set {propertyName} to true";
context.RegisterCodeFix(
CodeAction.Create(title, ct => Refactor(context.Document, creation, propertyName, ct), equivalenceKey: title),
diagnostic);
}
}

private static string? GetPropertyName(string diagnosticId) => diagnosticId switch
{
RuleIdentifiers.SetRespectNullableAnnotationsOnJsonSerializerOptions => "RespectNullableAnnotations",
RuleIdentifiers.SetRespectRequiredConstructorParametersOnJsonSerializerOptions => "RespectRequiredConstructorParameters",
_ => null,
};

private static async Task<Document> Refactor(Document document, BaseObjectCreationExpressionSyntax creation, string propertyName, CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

var assignment = SyntaxFactory.AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
SyntaxFactory.IdentifierName(propertyName),
SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression));

var initializer = creation.Initializer ?? SyntaxFactory.InitializerExpression(SyntaxKind.ObjectInitializerExpression);
editor.ReplaceNode(creation, creation.WithInitializer(initializer.AddExpressions(assignment)).WithAdditionalAnnotations(Formatter.Annotation));

return editor.GetChangedDocument();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,9 @@ dotnet_diagnostic.MA0222.severity = error

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = error

# MA0224: JsonSerializerOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0224.severity = error

# MA0225: JsonSerializerOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0225.severity = error
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,9 @@ dotnet_diagnostic.MA0222.severity = suggestion

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = suggestion

# MA0224: JsonSerializerOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0224.severity = suggestion

# MA0225: JsonSerializerOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0225.severity = suggestion
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,9 @@ dotnet_diagnostic.MA0222.severity = warning

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = warning

# MA0224: JsonSerializerOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0224.severity = warning

# MA0225: JsonSerializerOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0225.severity = warning
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,9 @@ dotnet_diagnostic.MA0222.severity = none

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = none

# MA0224: JsonSerializerOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0224.severity = none

# MA0225: JsonSerializerOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0225.severity = none
6 changes: 6 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,9 @@ dotnet_diagnostic.MA0222.severity = none

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = none

# MA0224: JsonSerializerOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0224.severity = none

# MA0225: JsonSerializerOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0225.severity = none
2 changes: 2 additions & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ internal static class RuleIdentifiers
public const string MissingMaybeNullWhenAttributeOnTryGetValue = "MA0221";
public const string SetRespectNullableAnnotations = "MA0222";
public const string SetRespectRequiredConstructorParameters = "MA0223";
public const string SetRespectNullableAnnotationsOnJsonSerializerOptions = "MA0224";
public const string SetRespectRequiredConstructorParametersOnJsonSerializerOptions = "MA0225";

public static string GetHelpUri(string identifier)
{
Expand Down
Loading