Preserve structured model binding and validation error information in ModelState
- 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
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