Skip to content
Open
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
39 changes: 35 additions & 4 deletions src/Validation/gen/Extensions/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,46 @@ attr.AttributeClass is not null &&
}

/// <summary>
/// Checks if the property is marked with [JsonIgnore] attribute.
/// Checks if the property is marked with [JsonIgnore] attribute with a condition that affects deserialization.
/// Only skips validation when the condition is Always or WhenReading, as these affect the reading/deserialization process.
/// Properties with conditions that only affect writing (WhenWritingDefault, WhenWritingNull, WhenWriting) or Never are still validated.
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation mentions "WhenWriting" as a JsonIgnoreCondition value, but this does not exist in the System.Text.Json.Serialization.JsonIgnoreCondition enum. The actual enum values are: Never, Always, WhenWritingDefault, WhenWritingNull, and WhenReading. Remove the reference to "WhenWriting" from the documentation.

Suggested change
/// Properties with conditions that only affect writing (WhenWritingDefault, WhenWritingNull, WhenWriting) or Never are still validated.
/// Properties with conditions that only affect writing (WhenWritingDefault, WhenWritingNull) or Never are still validated.

Copilot uses AI. Check for mistakes.
/// </summary>
/// <param name="property">The property to check.</param>
/// <param name="jsonIgnoreAttributeSymbol">The symbol representing the [JsonIgnore] attribute.</param>
internal static bool IsJsonIgnoredProperty(this IPropertySymbol property, INamedTypeSymbol jsonIgnoreAttributeSymbol)
{
return property.GetAttributes().Any(attr =>
attr.AttributeClass is not null &&
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol));
// JsonIgnoreCondition enum values from System.Text.Json.Serialization
const int JsonIgnoreCondition_Always = 1; // Property is always ignored
const int JsonIgnoreCondition_WhenReading = 5; // Property is ignored during deserialization

foreach (var attr in property.GetAttributes())
{
if (attr.AttributeClass is not null &&
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol))
{
// Check if the Condition property is set
if (!attr.NamedArguments.IsDefaultOrEmpty)
{
foreach (var namedArgument in attr.NamedArguments)
{
if (string.Equals(namedArgument.Key, "Condition", System.StringComparison.Ordinal))
{
// The value is an enum represented as an int
if (namedArgument.Value.Value is int conditionValue)
{
// Only skip validation for Always or WhenReading (conditions that affect reading/deserialization)
return conditionValue == JsonIgnoreCondition_Always || conditionValue == JsonIgnoreCondition_WhenReading;
}
}
}
}

// If no Condition is specified, the default behavior is Always (skip validation)
return true;
}
}

return false;
}

internal static bool IsSkippedValidationProperty(this IPropertySymbol property, INamedTypeSymbol skipValidationAttributeSymbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,172 @@ async Task ValidPublicPropertyStillValidated(Endpoint endpoint)
}
});
}

[Fact]
public async Task ValidatesPropertiesWithJsonIgnoreWhenWritingConditions()
{
// Arrange
var source = """
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder();

builder.Services.AddValidation();

var app = builder.Build();

app.MapPost("/json-ignore-conditions", (JsonIgnoreConditionsModel model) => Results.Ok("Passed"!));

app.Run();

public class JsonIgnoreConditionsModel
{
// JsonIgnore without Condition defaults to Always - should be ignored
[JsonIgnore]
[MaxLength(10)]
public string? PropertyWithJsonIgnoreOnly { get; set; }

// JsonIgnoreCondition.Always - should be ignored
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
[MaxLength(10)]
public string? PropertyWithAlways { get; set; }

// JsonIgnoreCondition.WhenWritingDefault - should be validated (only affects writing)
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
[MaxLength(10)]
public string? PropertyWithWhenWritingDefault { get; set; }

// JsonIgnoreCondition.WhenWritingNull - should be validated (only affects writing)
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[MaxLength(10)]
public string? PropertyWithWhenWritingNull { get; set; }

// JsonIgnoreCondition.Never - should be validated (never ignored)
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
[MaxLength(10)]
public string? PropertyWithNever { get; set; }

// JsonIgnoreCondition.WhenReading - should be ignored (affects reading/deserialization)
[JsonIgnore(Condition = JsonIgnoreCondition.WhenReading)]
[MaxLength(10)]
public string? PropertyWithWhenReading { get; set; }
}
""";
await Verify(source, out var compilation);
await VerifyEndpoint(compilation, "/json-ignore-conditions", async (endpoint, serviceProvider) =>
{
// Test that WhenWritingDefault is validated
await InvalidPropertyWithWhenWritingDefaultProducesError(endpoint);

// Test that WhenWritingNull is validated
await InvalidPropertyWithWhenWritingNullProducesError(endpoint);

// Test that Never is validated
await InvalidPropertyWithNeverProducesError(endpoint);

// Test that Always and JsonIgnore (without condition) are NOT validated (no error expected)
await InvalidPropertiesWithAlwaysAndDefaultAreIgnored(endpoint);

// Test that WhenReading is NOT validated (no error expected)
await InvalidPropertyWithWhenReadingIsIgnored(endpoint);

async Task InvalidPropertyWithWhenWritingDefaultProducesError(Endpoint endpoint)
{
var payload = """
{
"PropertyWithWhenWritingDefault": "ExceedsMaxLength"
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);

await endpoint.RequestDelegate(context);

var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("PropertyWithWhenWritingDefault", kvp.Key);
Assert.Contains("maximum length", kvp.Value.Single());
});
}

async Task InvalidPropertyWithWhenWritingNullProducesError(Endpoint endpoint)
{
var payload = """
{
"PropertyWithWhenWritingNull": "ExceedsMaxLength"
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);

await endpoint.RequestDelegate(context);

var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("PropertyWithWhenWritingNull", kvp.Key);
Assert.Contains("maximum length", kvp.Value.Single());
});
}

async Task InvalidPropertyWithNeverProducesError(Endpoint endpoint)
{
var payload = """
{
"PropertyWithNever": "ExceedsMaxLength"
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);

await endpoint.RequestDelegate(context);

var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("PropertyWithNever", kvp.Key);
Assert.Contains("maximum length", kvp.Value.Single());
});
}

async Task InvalidPropertiesWithAlwaysAndDefaultAreIgnored(Endpoint endpoint)
{
var payload = """
{
"PropertyWithJsonIgnoreOnly": "ExceedsMaxLength",
"PropertyWithAlways": "ExceedsMaxLength"
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);

await endpoint.RequestDelegate(context);

// Should succeed because these properties are ignored during validation
Assert.Equal(200, context.Response.StatusCode);
}

async Task InvalidPropertyWithWhenReadingIsIgnored(Endpoint endpoint)
{
var payload = """
{
"PropertyWithWhenReading": "ExceedsMaxLength"
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);

await endpoint.RequestDelegate(context);

// Should succeed because this property is ignored during reading/deserialization
Assert.Equal(200, context.Response.StatusCode);
}
});
}
}
Loading
Loading