Skip to content

Preserve structured model binding and validation error information in ModelState #68843

Description

@matra774

Is there an existing issue for this?

  • 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

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

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

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:

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

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

ModelState.TryAddModelError(key, result.Message);

ValidationVisitor.cs — validation results added to ModelState, ASP.NET Core v10.0.9, line 237

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

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:

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

ValidationResult
    ↓
DataAnnotationsModelValidator
    ↓
ModelValidationResult(MemberName, Message)
    ↓
ValidationVisitor
    ↓
ModelError(ErrorMessage)

JSON input with AllowInputFormatterExceptionMessages = true

JsonException
    ↓
InputFormatterException
    ↓
ModelStateDictionary
    ↓
ModelError(ErrorMessage)

Related issues include:

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions