Skip to content

Commit 5723334

Browse files
Add JsonIgnore attribute support to Minimal API validation generator (#63075)
* Initial plan * Add JsonIgnore attribute support to validation generator Co-authored-by: captainsafia <[email protected]> * Fix JsonIgnore test case to prevent circular reference stack overflow Co-authored-by: captainsafia <[email protected]> * Update CanValidateComplexTypesWithJsonIgnore test with record type cases Co-authored-by: captainsafia <[email protected]> * Remove unused IsJsonIgnoredParameter method and clean up test snapshots - Remove IsJsonIgnoredParameter method since JsonIgnore can only be applied to properties and fields, not constructor parameters - Update record parameter validation to check corresponding property for JsonIgnore instead - Delete stale received snapshot file - Update verified snapshot to include record type validation results Co-authored-by: captainsafia <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: captainsafia <[email protected]>
1 parent e507a91 commit 5723334

File tree

6 files changed

+355
-0
lines changed

6 files changed

+355
-0
lines changed

src/Shared/RoslynUtils/WellKnownTypeData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public enum WellKnownType
119119
Microsoft_AspNetCore_Authorization_IAuthorizeData,
120120
System_AttributeUsageAttribute,
121121
System_Text_Json_Serialization_JsonDerivedTypeAttribute,
122+
System_Text_Json_Serialization_JsonIgnoreAttribute,
122123
System_ComponentModel_DataAnnotations_DisplayAttribute,
123124
System_ComponentModel_DataAnnotations_ValidationAttribute,
124125
System_ComponentModel_DataAnnotations_RequiredAttribute,
@@ -240,6 +241,7 @@ public enum WellKnownType
240241
"Microsoft.AspNetCore.Authorization.IAuthorizeData",
241242
"System.AttributeUsageAttribute",
242243
"System.Text.Json.Serialization.JsonDerivedTypeAttribute",
244+
"System.Text.Json.Serialization.JsonIgnoreAttribute",
243245
"System.ComponentModel.DataAnnotations.DisplayAttribute",
244246
"System.ComponentModel.DataAnnotations.ValidationAttribute",
245247
"System.ComponentModel.DataAnnotations.RequiredAttribute",

src/Validation/gen/Extensions/ITypeSymbolExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,16 @@ attr.AttributeClass is not null &&
152152
(attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) ||
153153
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol)));
154154
}
155+
156+
/// <summary>
157+
/// Checks if the property is marked with [JsonIgnore] attribute.
158+
/// </summary>
159+
/// <param name="property">The property to check.</param>
160+
/// <param name="jsonIgnoreAttributeSymbol">The symbol representing the [JsonIgnore] attribute.</param>
161+
internal static bool IsJsonIgnoredProperty(this IPropertySymbol property, INamedTypeSymbol jsonIgnoreAttributeSymbol)
162+
{
163+
return property.GetAttributes().Any(attr =>
164+
attr.AttributeClass is not null &&
165+
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol));
166+
}
155167
}

src/Validation/gen/Models/RequiredSymbols.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal sealed record class RequiredSymbols(
1111
INamedTypeSymbol IEnumerable,
1212
INamedTypeSymbol IValidatableObject,
1313
INamedTypeSymbol JsonDerivedTypeAttribute,
14+
INamedTypeSymbol JsonIgnoreAttribute,
1415
INamedTypeSymbol RequiredAttribute,
1516
INamedTypeSymbol CustomValidationAttribute,
1617
INamedTypeSymbol HttpContext,

src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
114114
WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata);
115115
var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get(
116116
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
117+
var jsonIgnoreAttributeSymbol = wellKnownTypes.Get(
118+
WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonIgnoreAttribute);
117119

118120
// Special handling for record types to extract properties from
119121
// the primary constructor.
@@ -148,6 +150,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
148150
continue;
149151
}
150152

153+
// Skip properties that have JsonIgnore attribute
154+
if (correspondingProperty.IsJsonIgnoredProperty(jsonIgnoreAttributeSymbol))
155+
{
156+
continue;
157+
}
158+
151159
// Check if the property's type is validatable, this resolves
152160
// validatable types in the inheritance hierarchy
153161
var hasValidatableType = TryExtractValidatableType(
@@ -186,6 +194,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
186194
continue;
187195
}
188196

197+
// Skip properties that have JsonIgnore attribute
198+
if (member.IsJsonIgnoredProperty(jsonIgnoreAttributeSymbol))
199+
{
200+
continue;
201+
}
202+
189203
var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
190204
var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);
191205

src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,153 @@ namespace Microsoft.Extensions.Validation.GeneratorTests;
77

88
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
99
{
10+
[Fact]
11+
public async Task CanValidateComplexTypesWithJsonIgnore()
12+
{
13+
// Arrange
14+
var source = """
15+
using System;
16+
using System.ComponentModel.DataAnnotations;
17+
using System.Collections.Generic;
18+
using System.Threading.Tasks;
19+
using Microsoft.AspNetCore.Builder;
20+
using Microsoft.AspNetCore.Http;
21+
using Microsoft.Extensions.Validation;
22+
using Microsoft.AspNetCore.Routing;
23+
using Microsoft.Extensions.DependencyInjection;
24+
using Microsoft.AspNetCore.Mvc;
25+
using System.Text.Json.Serialization;
26+
27+
var builder = WebApplication.CreateBuilder();
28+
29+
builder.Services.AddValidation();
30+
31+
var app = builder.Build();
32+
33+
app.MapPost("/complex-type-with-json-ignore", (ComplexTypeWithJsonIgnore complexType) => Results.Ok("Passed"!));
34+
app.MapPost("/record-type-with-json-ignore", (RecordTypeWithJsonIgnore recordType) => Results.Ok("Passed"!));
35+
36+
app.Run();
37+
38+
public class ComplexTypeWithJsonIgnore
39+
{
40+
[Range(10, 100)]
41+
public int ValidatedProperty { get; set; } = 10;
42+
43+
[JsonIgnore]
44+
[Required] // This should be ignored because of [JsonIgnore]
45+
public string IgnoredProperty { get; set; } = null!;
46+
47+
[JsonIgnore]
48+
public CircularReferenceType? CircularReference { get; set; }
49+
}
50+
51+
public class CircularReferenceType
52+
{
53+
[JsonIgnore]
54+
public ComplexTypeWithJsonIgnore? Parent { get; set; }
55+
56+
public string Name { get; set; } = "test";
57+
}
58+
59+
public record RecordTypeWithJsonIgnore
60+
{
61+
[Range(10, 100)]
62+
public int ValidatedProperty { get; set; } = 10;
63+
64+
[JsonIgnore]
65+
[Required] // This should be ignored because of [JsonIgnore]
66+
public string IgnoredProperty { get; set; } = null!;
67+
68+
[JsonIgnore]
69+
public CircularReferenceRecord? CircularReference { get; set; }
70+
}
71+
72+
public record CircularReferenceRecord
73+
{
74+
[JsonIgnore]
75+
public RecordTypeWithJsonIgnore? Parent { get; set; }
76+
77+
public string Name { get; set; } = "test";
78+
}
79+
""";
80+
await Verify(source, out var compilation);
81+
await VerifyEndpoint(compilation, "/complex-type-with-json-ignore", async (endpoint, serviceProvider) =>
82+
{
83+
await ValidInputWithJsonIgnoreProducesNoWarnings(endpoint);
84+
await InvalidValidatedPropertyProducesError(endpoint);
85+
86+
async Task ValidInputWithJsonIgnoreProducesNoWarnings(Endpoint endpoint)
87+
{
88+
var payload = """
89+
{
90+
"ValidatedProperty": 50
91+
}
92+
""";
93+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
94+
await endpoint.RequestDelegate(context);
95+
96+
Assert.Equal(200, context.Response.StatusCode);
97+
}
98+
99+
async Task InvalidValidatedPropertyProducesError(Endpoint endpoint)
100+
{
101+
var payload = """
102+
{
103+
"ValidatedProperty": 5
104+
}
105+
""";
106+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
107+
108+
await endpoint.RequestDelegate(context);
109+
110+
var problemDetails = await AssertBadRequest(context);
111+
Assert.Collection(problemDetails.Errors, kvp =>
112+
{
113+
Assert.Equal("ValidatedProperty", kvp.Key);
114+
Assert.Equal("The field ValidatedProperty must be between 10 and 100.", kvp.Value.Single());
115+
});
116+
}
117+
});
118+
119+
await VerifyEndpoint(compilation, "/record-type-with-json-ignore", async (endpoint, serviceProvider) =>
120+
{
121+
await ValidInputWithJsonIgnoreProducesNoWarningsForRecord(endpoint);
122+
await InvalidValidatedPropertyProducesErrorForRecord(endpoint);
123+
124+
async Task ValidInputWithJsonIgnoreProducesNoWarningsForRecord(Endpoint endpoint)
125+
{
126+
var payload = """
127+
{
128+
"ValidatedProperty": 50
129+
}
130+
""";
131+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
132+
await endpoint.RequestDelegate(context);
133+
134+
Assert.Equal(200, context.Response.StatusCode);
135+
}
136+
137+
async Task InvalidValidatedPropertyProducesErrorForRecord(Endpoint endpoint)
138+
{
139+
var payload = """
140+
{
141+
"ValidatedProperty": 5
142+
}
143+
""";
144+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
145+
146+
await endpoint.RequestDelegate(context);
147+
148+
var problemDetails = await AssertBadRequest(context);
149+
Assert.Collection(problemDetails.Errors, kvp =>
150+
{
151+
Assert.Equal("ValidatedProperty", kvp.Key);
152+
Assert.Equal("The field ValidatedProperty must be between 10 and 100.", kvp.Value.Single());
153+
});
154+
}
155+
});
156+
}
10157
[Fact]
11158
public async Task CanValidateComplexTypes()
12159
{

0 commit comments

Comments
 (0)