dotnet / dotnet/aspnetcore

Customizing the validation error response shape

Open
#68,705 1 comment 0 reactions 0 assignees View on GitHub
api-proposal api-suggestion area-minimal
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 5h
Merged PRs (30d)
276

Description

## Background and Motivation

I want to use data annotations validation but instead of returning a response body like:

```json
{
"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:

```json
{
"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

```diff
namespace Microsoft.Extensions.Validation;

public sealed class ValidationError
{
+ public ValidationAttribute? ValidationAttribute { get; init; }
}

+public interface IValidationErrorsHandler
+{
+ ValueTask TryHandleAsync(HttpContext httpContext, IReadOnlyDictionary>, CancellationToken cancellationToken);
+}
```

## Usage Examples

```csharp
public sealed record CodedError(
string Path,
string Type,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
IReadOnlyDictionary? Params,
string Detail);

internal sealed class CodedValidationErrorsHandler(IOptions jsonOptions) : IValidationErrorsHandler
{
private readonly JsonNamingPolicy _naming =
jsonOptions.Value.SerializerOptions.PropertyNamingPolicy ?? JsonNamingPolicy.CamelCase;

public async ValueTask TryHandleAsync(
HttpContext httpContext,
IReadOnlyDictionary> 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? 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`](https://github.com/dotnet/aspnetcore/blob/e072299dd7ec1733e9fb20c60993c5a48208c625/src/Http/Routing/src/ValidationEndpointFilterFactory.cs) 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.

Contributor guide

Open the contributing guide

Research direction

Start by reading src/Http/Routing/src/ValidationEndpointFilterFactory.cs and the existing ValidationError type in the Microsoft.Extensions.Validation area. Compare the proposed ValidationAttribute and IValidationErrorsHandler APIs with the current validation endpoint flow, then use the API review process to establish an agreed design and acceptance tests for customizable response handling.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
api, backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.