Background and Motivation
I want to use data annotations validation but instead of returning a response body like:
{
"title": "One or more validation errors occurred.",
"errors": {
"Name": ["The field Name must be a string with a minimum length of 2 and a maximum length of 20."],
"Email": ["The Email field is not a valid e-mail address."],
"Age": ["The field Age must be between 18 and 120."]
}
}
The API also gives enough flexibility to make it pretty easy to localize those error messages but it does not make it easy to make it so you could localize those error messages on the client side, where a lot of single page applications will do their internationalization. I would love for enough API flexibility to return an error like:
{
"type": "https://problems.example.com/validation-failed",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": [
{
"path": "/name",
"type": "invalid_length",
"params": { "min": 2, "max": 20 },
"detail": "The field Name must be a string with a minimum length of 2 and a maximum length of 20."
},
{
"path": "/email",
"type": "invalid_format",
"params": { "format": "email" },
"detail": "The Email field is not a valid e-mail address."
},
{
"path": "/age",
"code": "invalid_range_value",
"params": { "min": 18, "max": 120 },
"detail": "The field Age must be between 18 and 120."
}
]
}
Proposed API
namespace Microsoft.Extensions.Validation;
public sealed class ValidationError
{
+ public ValidationAttribute? ValidationAttribute { get; init; }
}
+public interface IValidationErrorsHandler
+{
+ ValueTask<bool> TryHandleAsync(HttpContext httpContext, IReadOnlyDictionary<string, IReadOnlyList<ValidationError>>, CancellationToken cancellationToken);
+}
Usage Examples
public sealed record CodedError(
string Path,
string Type,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
IReadOnlyDictionary<string, object?>? Params,
string Detail);
internal sealed class CodedValidationErrorsHandler(IOptions<JsonOptions> jsonOptions) : IValidationErrorsHandler
{
private readonly JsonNamingPolicy _naming =
jsonOptions.Value.SerializerOptions.PropertyNamingPolicy ?? JsonNamingPolicy.CamelCase;
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
IReadOnlyDictionary<string, IReadOnlyList<ValidationError>> errors,
CancellationToken cancellationToken)
{
var problem = new CodedValidationProblem(
Type: "https://problems.example.com/validation-failed",
Title: "One or more validation errors occurred.",
Status: StatusCodes.Status400BadRequest,
TraceId: Activity.Current?.Id ?? httpContext.TraceIdentifier,
Errors: errors
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
.SelectMany(entry => entry.Value.Select(Map))
.ToArray());
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
await httpContext.Response.WriteAsJsonAsync(
problem, s_options, contentType: "application/problem+json", cancellationToken);
return true;
}
private CodedError Map(ValidationError error)
{
var (type, parameters) = Describe(error.ValidationAttribute);
return new CodedError(ToJsonPointer(error.Path), type, parameters, error.ErrorMessage);
}
private static (string Type, IReadOnlyDictionary<string, object?>? Params) Describe(ValidationAttribute? attribute) =>
attribute switch
{
StringLengthAttribute a => ("invalid_length", P(("min", Positive(a.MinimumLength)), ("max", a.MaximumLength))),
EmailAddressAttribute => ("invalid_format", P(("format", "email"))),
RangeAttribute a => ("invalid_range_value", P(("min", a.Minimum), ("max", a.Maximum))),
// ... required, invalid_value, mismatch, and the remaining formats
// null: IValidatableObject, or a source we have no mapping for.
_ => ("invalid", null)
};
}
Alternative Designs
If you did just the ValidationAttribute addition to ValidateionError then I could build my own validation endpoint filter based on the source code here and still get the benefits of the source generator. The IValidationErrorsHandler exists just so I can take advantage of 99% ValidationEndpointFilterFactory without rewriting it all.
Risks
The ValidationAttribute property probably can't be made required since its a public shipped type already and so it will be optional but the internal implementation would assign it whenever available.
Background and Motivation
I want to use data annotations validation but instead of returning a response body like:
{ "title": "One or more validation errors occurred.", "errors": { "Name": ["The field Name must be a string with a minimum length of 2 and a maximum length of 20."], "Email": ["The Email field is not a valid e-mail address."], "Age": ["The field Age must be between 18 and 120."] } }The API also gives enough flexibility to make it pretty easy to localize those error messages but it does not make it easy to make it so you could localize those error messages on the client side, where a lot of single page applications will do their internationalization. I would love for enough API flexibility to return an error like:
{ "type": "https://problems.example.com/validation-failed", "title": "One or more validation errors occurred.", "status": 400, "errors": [ { "path": "/name", "type": "invalid_length", "params": { "min": 2, "max": 20 }, "detail": "The field Name must be a string with a minimum length of 2 and a maximum length of 20." }, { "path": "/email", "type": "invalid_format", "params": { "format": "email" }, "detail": "The Email field is not a valid e-mail address." }, { "path": "/age", "code": "invalid_range_value", "params": { "min": 18, "max": 120 }, "detail": "The field Age must be between 18 and 120." } ] }Proposed API
namespace Microsoft.Extensions.Validation; public sealed class ValidationError { + public ValidationAttribute? ValidationAttribute { get; init; } } +public interface IValidationErrorsHandler +{ + ValueTask<bool> TryHandleAsync(HttpContext httpContext, IReadOnlyDictionary<string, IReadOnlyList<ValidationError>>, CancellationToken cancellationToken); +}Usage Examples
Alternative Designs
If you did just the ValidationAttribute addition to ValidateionError then I could build my own validation endpoint filter based on the source code here and still get the benefits of the source generator. The
IValidationErrorsHandlerexists just so I can take advantage of 99%ValidationEndpointFilterFactorywithout rewriting it all.Risks
The ValidationAttribute property probably can't be made required since its a public shipped type already and so it will be optional but the internal implementation would assign it whenever available.