Skip to content

Commit 7ecd485

Browse files
Fix JsonIgnore validation bypass for write-only conditions (#64729)
* Initial plan * Fix JsonIgnore validation to only skip for Always and WhenReading conditions Co-authored-by: captainsafia <[email protected]> * Add constants for JsonIgnoreCondition values to improve code maintainability Co-authored-by: captainsafia <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: captainsafia <[email protected]>
1 parent e5a3d79 commit 7ecd485

File tree

3 files changed

+404
-4
lines changed

3 files changed

+404
-4
lines changed

src/Validation/gen/Extensions/ITypeSymbolExtensions.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,15 +155,46 @@ attr.AttributeClass is not null &&
155155
}
156156

157157
/// <summary>
158-
/// Checks if the property is marked with [JsonIgnore] attribute.
158+
/// Checks if the property is marked with [JsonIgnore] attribute with a condition that affects deserialization.
159+
/// Only skips validation when the condition is Always or WhenReading, as these affect the reading/deserialization process.
160+
/// Properties with conditions that only affect writing (WhenWritingDefault, WhenWritingNull, WhenWriting) or Never are still validated.
159161
/// </summary>
160162
/// <param name="property">The property to check.</param>
161163
/// <param name="jsonIgnoreAttributeSymbol">The symbol representing the [JsonIgnore] attribute.</param>
162164
internal static bool IsJsonIgnoredProperty(this IPropertySymbol property, INamedTypeSymbol jsonIgnoreAttributeSymbol)
163165
{
164-
return property.GetAttributes().Any(attr =>
165-
attr.AttributeClass is not null &&
166-
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol));
166+
// JsonIgnoreCondition enum values from System.Text.Json.Serialization
167+
const int JsonIgnoreCondition_Always = 1; // Property is always ignored
168+
const int JsonIgnoreCondition_WhenReading = 5; // Property is ignored during deserialization
169+
170+
foreach (var attr in property.GetAttributes())
171+
{
172+
if (attr.AttributeClass is not null &&
173+
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol))
174+
{
175+
// Check if the Condition property is set
176+
if (!attr.NamedArguments.IsDefaultOrEmpty)
177+
{
178+
foreach (var namedArgument in attr.NamedArguments)
179+
{
180+
if (string.Equals(namedArgument.Key, "Condition", System.StringComparison.Ordinal))
181+
{
182+
// The value is an enum represented as an int
183+
if (namedArgument.Value.Value is int conditionValue)
184+
{
185+
// Only skip validation for Always or WhenReading (conditions that affect reading/deserialization)
186+
return conditionValue == JsonIgnoreCondition_Always || conditionValue == JsonIgnoreCondition_WhenReading;
187+
}
188+
}
189+
}
190+
}
191+
192+
// If no Condition is specified, the default behavior is Always (skip validation)
193+
return true;
194+
}
195+
}
196+
197+
return false;
167198
}
168199

169200
internal static bool IsSkippedValidationProperty(this IPropertySymbol property, INamedTypeSymbol skipValidationAttributeSymbol)

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

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,4 +623,172 @@ async Task ValidPublicPropertyStillValidated(Endpoint endpoint)
623623
}
624624
});
625625
}
626+
627+
[Fact]
628+
public async Task ValidatesPropertiesWithJsonIgnoreWhenWritingConditions()
629+
{
630+
// Arrange
631+
var source = """
632+
using System;
633+
using System.ComponentModel.DataAnnotations;
634+
using System.Collections.Generic;
635+
using System.Threading.Tasks;
636+
using Microsoft.AspNetCore.Builder;
637+
using Microsoft.AspNetCore.Http;
638+
using Microsoft.Extensions.Validation;
639+
using Microsoft.AspNetCore.Routing;
640+
using Microsoft.Extensions.DependencyInjection;
641+
using Microsoft.AspNetCore.Mvc;
642+
using System.Text.Json.Serialization;
643+
644+
var builder = WebApplication.CreateBuilder();
645+
646+
builder.Services.AddValidation();
647+
648+
var app = builder.Build();
649+
650+
app.MapPost("/json-ignore-conditions", (JsonIgnoreConditionsModel model) => Results.Ok("Passed"!));
651+
652+
app.Run();
653+
654+
public class JsonIgnoreConditionsModel
655+
{
656+
// JsonIgnore without Condition defaults to Always - should be ignored
657+
[JsonIgnore]
658+
[MaxLength(10)]
659+
public string? PropertyWithJsonIgnoreOnly { get; set; }
660+
661+
// JsonIgnoreCondition.Always - should be ignored
662+
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
663+
[MaxLength(10)]
664+
public string? PropertyWithAlways { get; set; }
665+
666+
// JsonIgnoreCondition.WhenWritingDefault - should be validated (only affects writing)
667+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
668+
[MaxLength(10)]
669+
public string? PropertyWithWhenWritingDefault { get; set; }
670+
671+
// JsonIgnoreCondition.WhenWritingNull - should be validated (only affects writing)
672+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
673+
[MaxLength(10)]
674+
public string? PropertyWithWhenWritingNull { get; set; }
675+
676+
// JsonIgnoreCondition.Never - should be validated (never ignored)
677+
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
678+
[MaxLength(10)]
679+
public string? PropertyWithNever { get; set; }
680+
681+
// JsonIgnoreCondition.WhenReading - should be ignored (affects reading/deserialization)
682+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenReading)]
683+
[MaxLength(10)]
684+
public string? PropertyWithWhenReading { get; set; }
685+
}
686+
""";
687+
await Verify(source, out var compilation);
688+
await VerifyEndpoint(compilation, "/json-ignore-conditions", async (endpoint, serviceProvider) =>
689+
{
690+
// Test that WhenWritingDefault is validated
691+
await InvalidPropertyWithWhenWritingDefaultProducesError(endpoint);
692+
693+
// Test that WhenWritingNull is validated
694+
await InvalidPropertyWithWhenWritingNullProducesError(endpoint);
695+
696+
// Test that Never is validated
697+
await InvalidPropertyWithNeverProducesError(endpoint);
698+
699+
// Test that Always and JsonIgnore (without condition) are NOT validated (no error expected)
700+
await InvalidPropertiesWithAlwaysAndDefaultAreIgnored(endpoint);
701+
702+
// Test that WhenReading is NOT validated (no error expected)
703+
await InvalidPropertyWithWhenReadingIsIgnored(endpoint);
704+
705+
async Task InvalidPropertyWithWhenWritingDefaultProducesError(Endpoint endpoint)
706+
{
707+
var payload = """
708+
{
709+
"PropertyWithWhenWritingDefault": "ExceedsMaxLength"
710+
}
711+
""";
712+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
713+
714+
await endpoint.RequestDelegate(context);
715+
716+
var problemDetails = await AssertBadRequest(context);
717+
Assert.Collection(problemDetails.Errors, kvp =>
718+
{
719+
Assert.Equal("PropertyWithWhenWritingDefault", kvp.Key);
720+
Assert.Contains("maximum length", kvp.Value.Single());
721+
});
722+
}
723+
724+
async Task InvalidPropertyWithWhenWritingNullProducesError(Endpoint endpoint)
725+
{
726+
var payload = """
727+
{
728+
"PropertyWithWhenWritingNull": "ExceedsMaxLength"
729+
}
730+
""";
731+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
732+
733+
await endpoint.RequestDelegate(context);
734+
735+
var problemDetails = await AssertBadRequest(context);
736+
Assert.Collection(problemDetails.Errors, kvp =>
737+
{
738+
Assert.Equal("PropertyWithWhenWritingNull", kvp.Key);
739+
Assert.Contains("maximum length", kvp.Value.Single());
740+
});
741+
}
742+
743+
async Task InvalidPropertyWithNeverProducesError(Endpoint endpoint)
744+
{
745+
var payload = """
746+
{
747+
"PropertyWithNever": "ExceedsMaxLength"
748+
}
749+
""";
750+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
751+
752+
await endpoint.RequestDelegate(context);
753+
754+
var problemDetails = await AssertBadRequest(context);
755+
Assert.Collection(problemDetails.Errors, kvp =>
756+
{
757+
Assert.Equal("PropertyWithNever", kvp.Key);
758+
Assert.Contains("maximum length", kvp.Value.Single());
759+
});
760+
}
761+
762+
async Task InvalidPropertiesWithAlwaysAndDefaultAreIgnored(Endpoint endpoint)
763+
{
764+
var payload = """
765+
{
766+
"PropertyWithJsonIgnoreOnly": "ExceedsMaxLength",
767+
"PropertyWithAlways": "ExceedsMaxLength"
768+
}
769+
""";
770+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
771+
772+
await endpoint.RequestDelegate(context);
773+
774+
// Should succeed because these properties are ignored during validation
775+
Assert.Equal(200, context.Response.StatusCode);
776+
}
777+
778+
async Task InvalidPropertyWithWhenReadingIsIgnored(Endpoint endpoint)
779+
{
780+
var payload = """
781+
{
782+
"PropertyWithWhenReading": "ExceedsMaxLength"
783+
}
784+
""";
785+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
786+
787+
await endpoint.RequestDelegate(context);
788+
789+
// Should succeed because this property is ignored during reading/deserialization
790+
Assert.Equal(200, context.Response.StatusCode);
791+
}
792+
});
793+
}
626794
}

0 commit comments

Comments
 (0)