---
title: Project model-state errors into view models
summary: Translate ASP.NET model-state validation errors into JSON-friendly, grid-row-correlated view models.
status: draft
owner: SAIF Documentation
last_reviewed: 2026-09-17
library: SAIF.Framework.Web.Mvc
reference_type: package
operation_id: OP-16
trigger: Controller action returning validation results, typically as JSON
---

# Project model-state errors into view models

Translate ASP.NET model-state validation errors into JSON-friendly, grid-row-correlated view models. Trigger: Controller action returning validation results, typically as JSON.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-16 |
| **Trigger** | Controller action returning validation results, typically as JSON |
| **Inputs** | `ModelStateDictionary`, optional grid property and row-id expressions |
| **Outputs** | `ErrorViewModel[]` |
| **Primary components** | `ModelStateExtensions.GetErrors` overload family (`GetErrors(ModelStateDictionary)`, `GetErrors<TGridRow, TProperty>(ModelStateDictionary, Expression, Expression)`, `GetErrors<TGridRow, TProperty>(ModelStateDictionary, params GridErrorArguments<TGridRow, TProperty>[])`) |
| **Entry point** | `src/Web.Mvc/Extensions/ModelStateExtensions.cs` |

## Steps

1. `GetErrors()` (no arguments) uses a grid-row-id selector that always returns `""`, disabling grid correlation.
2. The grid overloads build `GridErrorArguments` from lambda expressions, capturing the grid property name and row-id property name through `ExpressionHelper.GetExpressionText`.
3. For each model-state key, the `gridRowKey` selector picks the first translator whose `GridName` prefixes the key and whose key does not contain `.Added[`, `.Deleted[`, or `.Updated[` — pending-change collections are excluded from grid error correlation ([BR-34](../business-rules.md#br-34-pending-change-grid-keys-are-not-correlated-to-a-row)).
4. `GridRowKey` normalizes the key: truncate at the last `]`; when the key already targets the view-model path (`{grid}.Rows[`) use it directly if it ends with the row-id name, otherwise append `.{rowIdName}`; when it targets the domain-model path, insert `.Rows` after the grid name before applying the same rule ([BR-35](../business-rules.md#br-35-grid-error-key-normalized-before-the-row-id-is-read)).
5. `GridRowId` resolves the normalized key back through `ModelState` and reads its `AttemptedValue` as the row identifier, or `""` when no grid key applies.
6. Emit one `ErrorViewModel` per model-state entry that has errors, with `Key` having `.` replaced by `_` for DOM friendliness, `Values` set to all error messages, and `GridRowId` set to the resolved row id ([BR-36](../business-rules.md#br-36-error-keys-are-dom-safe)).
7. `LoadValidationErrors(ModelStateDictionary, IValidationResult)` is the inbound counterpart: it adds one model error per `result.Errors` entry using `PropertyName` and `ErrorMessage`.

## Process flow

The following diagram visualizes this operation's control flow.

```mermaid
flowchart TD
	Start([Controller returns validation result])
	Args[Build GridErrorArguments from lambdas]
	Key[Select grid translator per model-state key]
	Norm[Normalize key via GridRowKey]
	RowId[Resolve GridRowId from ModelState]
	Emit[Emit ErrorViewModel array]
	EndOk([Return ErrorViewModel[]])
	Start --> Args
	Args --> Key
	Key --> Norm
	Norm --> RowId
	RowId --> Emit
	Emit --> EndOk
```

## Failure and edge-case behavior

- No unexpected failure paths beyond argument-guard exceptions and delegated dependency behavior were identified for this operation.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

## Sources
- `src/Web.Mvc/Extensions/ModelStateExtensions.cs`
