dotnet / dotnet/aspnetcore

Preserve structured model binding and validation error information in ModelState

Open
#68,843 1 comment 1 reaction 0 assignees View on GitHub
area-mvc
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Is your feature request related to a problem? Please describe the problem.

ASP.NET Core MVC frequently converts structured model-binding and validation errors into strings before application code can process them.

This makes it difficult to separate:

* diagnostic information that should be logged,
* machine-readable information that may safely be returned to clients, such as an error code or JSON path,
* potentially sensitive information that should not be exposed.

For example, `System.Text.Json.JsonException` contains structured information such as `Path`, `LineNumber` and `BytePositionInLine`.

`SystemTextJsonInputFormatter` catches the original `JsonException`, obtains its path, passes the exception through `WrapExceptionForModelState`, and adds the resulting exception to `ModelState`:

[SystemTextJsonInputFormatter.cs — `JsonException` handling, ASP.NET Core v10.0.9, lines 88–99](https://github.com/dotnet/aspnetcore/blob/v10.0.9/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs#L88-L99)

The same class contains `WrapExceptionForModelState(JsonException)`. When `JsonOptions.AllowInputFormatterExceptionMessages` is enabled it returns an `InputFormatterException` containing the original `JsonException`; otherwise it returns the original exception:

[SystemTextJsonInputFormatter.cs — `WrapExceptionForModelState`, ASP.NET Core v10.0.9, lines 133–144](https://github.com/dotnet/aspnetcore/blob/v10.0.9/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs#L133-L144)

`ModelStateDictionary.TryAddModelError(string, Exception, ModelMetadata)` explicitly treats `InputFormatterException` as a signal that its message is safe to expose to clients and replaces the exception with only `exception.Message`:

[ModelStateDictionary.cs — `InputFormatterException` handling, ASP.NET Core v10.0.9, lines 305-310](https://github.com/dotnet/aspnetcore/blob/v10.0.9/src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs#L305-L310)

Setting `AllowInputFormatterExceptionMessages = false` preserves the original `JsonException`, but this is a JSON-specific and non-obvious mechanism. An application may instead want to log the complete exception, return a safe property such as the JSON path, but suppress sensitive information contained in the exception message, such as an internal CLR type name.

A similar loss of structured information occurs during DataAnnotations validation.

For example, an application may return a custom `ValidationResult` containing a machine-readable error code:

```csharp
class ApiValidationResult : ValidationResult
{
public string ErrorCode { get; init; }
}
```

`DataAnnotationsModelValidator.Validate` receives the original `ValidationResult` from `ValidationAttribute.GetValidationResult`, but subsequently creates new `ModelValidationResult` instances containing only the member name and error message:

[DataAnnotationsModelValidator.cs — `ValidationResult` to `ModelValidationResult` conversion, ASP.NET Core v10.0.9, lines 99–123](https://github.com/dotnet/aspnetcore/blob/v10.0.9/src/Mvc/Mvc.DataAnnotations/src/DataAnnotationsModelValidator.cs#L99-L123)

`ValidationVisitor.ValidateNode` then reduces this representation further by adding only `result.Message` to `ModelState`:

```csharp
ModelState.TryAddModelError(key, result.Message);
```

[ValidationVisitor.cs — validation results added to `ModelState`, ASP.NET Core v10.0.9, line 237](https://github.com/dotnet/aspnetcore/blob/v10.0.9/src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidationVisitor.cs#L237)

Consequently, application code in `InvalidModelStateResponseFactory` cannot access `ErrorCode` or other structured information originally provided by the validation result.

This is also a longstanding use case. For example, this Stack Overflow question asks specifically how to return an error code in a custom `ValidationResult` and access it after ASP.NET Core validation:

[Stack Overflow — Return Custom ValidationResult and access it from controller in ASP.NET Core](https://stackoverflow.com/questions/51818978/return-custom-validationresult-and-access-it-from-controller-in-asp-net-core)

Once structured information has been flattened into an error-message string, applications cannot reliably decide which portions should be exposed to clients and which should only be logged.

### Describe the solution you'd like

Provide a general mechanism for model-binding and validation errors to preserve structured information through `ModelState`.

The exact API is open for discussion. Possible approaches include:

* allowing `ModelError` / `ModelValidationResult` to carry structured metadata or the original error object, or
* providing another structured error abstraction that survives until `InvalidModelStateResponseFactory`.

The important requirement is that ASP.NET Core should avoid irreversibly reducing an error to a string before application code has an opportunity to apply its own logging, security and API-response policy.

For example, an application should be able to produce:

```text
Server log:
original exception + complete diagnostic information

Client:
field/path: $.customer.type
code: INVALID_VALUE
message: Invalid value
```

without parsing framework-generated error-message strings.

The goal is not to expose more information by default. The goal is the opposite: **preserve structured information internally long enough for applications to make an explicit security decision about what to log and what to expose.**

### Additional context

The current ASP.NET Core 10 implementation demonstrates two related lossy paths.

**DataAnnotations**

```text
ValidationResult

DataAnnotationsModelValidator

ModelValidationResult(MemberName, Message)

ValidationVisitor

ModelError(ErrorMessage)
```

**JSON input with AllowInputFormatterExceptionMessages = true**

```text
JsonException

InputFormatterException

ModelStateDictionary

ModelError(ErrorMessage)
```

Related issues include:

* [#19485 — System.Text.Json leaks trace when a validation error occurs](https://github.com/dotnet/aspnetcore/issues/19485), demonstrating the need to control which parsing details are exposed to clients. The internal type names (including namespaces) are now leaked.
* [#15272 — JSON input formatter exceptions should log error not debug](https://github.com/dotnet/aspnetcore/issues/15272), demonstrating the corresponding need for useful diagnostic information server-side, without leaking too much information to the client.
* [#46349 — Support unified model for DataAnnotations-based validation via source generator](https://github.com/dotnet/aspnetcore/issues/46349), related to the broader validation infrastructure.

Contributor guide

Open the contributing guide

Research direction

Read SystemTextJsonInputFormatter.cs, ModelStateDictionary.cs, DataAnnotationsModelValidator.cs, and ValidationVisitor.cs, focusing on the linked error-handling paths and entry points. Trace how JsonException and ValidationResult become ModelState errors; done means structured information remains available through InvalidModelStateResponseFactory without exposing sensitive details by default.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.