A backend validation response is different from a transport failure. For a validation response such as HTTP 422, translate its field paths into keyed errors so ordinary field bindings can render them:
try {
await api.save(form.value.toMap());
} on ApiException catch (error) {
if (error.statusCode == 422) {
form.setServerErrorPaths(
Map<String, String>.from(error.responseBody['errors']),
);
} else {
showTransportError(error);
}
}
setServerErrors(Map<FieldKey, String>) merges messages into the current error map, force-reveals those keys, sets
submitted, and notifies listeners. Existing errors not replaced by the response remain. A transport exception should be shown as a request-level problem, not attached to a field unless the server supplied a field validation verdict.
setServerErrorPaths(Map<String, String>) parses the wire representation of FieldKey.toPath. Invalid path syntax throws
FormatException; catch or reject malformed backend data at the API boundary rather than assuming every string is usable.
Dynamic list row paths#
Address a list row by its stable clientId, never by its current index. For example, a city field path has the form
stops.['row-42'].city. The row id must be the actual clientId used by the model and generated row reference. Positional paths such as
stops.0.city do not identify a stable row and do not map to that row's field. If the backend returns row-specific errors, include a correlation/client identifier in the request so the response can refer to the same row identity.
form.setServerErrorPaths({
"stops.['${stop.clientId}'].city": 'City is not recognised',
});
The errors are merged and revealed like other server errors. A later local resolver run only replaces the scope it validates; use the appropriate validation/error lifecycle when reconciling a server verdict with edits. For pre-submit availability checks see Async validation.