The validation error response shape determines how Angular handles errors. If the shape is inconsistent — sometimes a string, sometimes an array, sometimes nested — Angular’s error interceptor must handle multiple cases, leading to bugs and duplicated error-handling code. Standardising on the RFC 7807 ValidationProblemDetails format for all 400 responses (both from automatic validation and manual validation in action bodies) gives Angular one shape to parse regardless of which layer rejected the request.
ValidationProblemDetails Format
// ── Standard ValidationProblemDetails response shape ─────────────────────
// HTTP 400 Bad Request
// Content-Type: application/problem+json
//
// {
// "type": "https://tools.ietf.org/html/rfc7807",
// "title": "One or more validation errors occurred.",
// "status": 400,
// "traceId": "00-abc123...",
// "errors": {
// "title": ["The title field is required.", "Title must be at least 5 characters."],
// "slug": ["Slug must contain only lowercase letters, numbers, and hyphens."],
// "address.city": ["City is required."],
// "tags[1]": ["Tag must not exceed 50 characters."]
// }
// }
// ── Consistent ValidationProblem() usage in action body ────────────────────
[HttpPost]
public async Task<ActionResult<PostDto>> Create(
CreatePostRequest request, CancellationToken ct)
{
// Async business validation — add to ModelState, then return
if (await _service.SlugExistsAsync(request.Slug, ct))
ModelState.AddModelError(nameof(request.Slug).ToLower(),
"This slug is already in use.");
if (await _service.UserHasReachedPostLimitAsync(User.GetUserId(), ct))
ModelState.AddModelError(string.Empty, // model-level, not field-specific
"You have reached your monthly post limit.");
if (!ModelState.IsValid)
return ValidationProblem(ModelState); // consistent 400 shape
var post = await _service.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = post.Id }, post);
}
errors dictionary keys use the same casing as the JSON property names in the request DTO. With PropertyNamingPolicy = JsonNamingPolicy.CamelCase, request properties are camelCase in JSON, so error keys are also camelCase: "title", "slug", "address.city". Angular’s reactive form uses the same camelCase property names. This alignment allows Angular to map errors["title"] directly to the title form control error without any transformation.traceId extension to all ValidationProblemDetails responses so users can report specific failed requests. When a user says “I got a validation error creating a post,” support can look up the traceId in the server logs to see the exact request details. The traceId from HttpContext.TraceIdentifier correlates with the structured log entry for that request. Add it in the custom InvalidModelStateResponseFactory (shown in Lesson 1 of this chapter).ModelState.AddModelError(string.Empty, "message")) appear in the errors dictionary with an empty string key or a key of "". Some Angular error parsers iterate over the errors dictionary and display field-specific messages next to fields — they ignore the empty-key entry. Always have a fallback in Angular’s error handler to display model-level errors in a summary section at the top of the form, separate from field-specific errors.Angular Error Interceptor Pattern
// ── TypeScript — Angular HttpInterceptor for validation errors ────────────
// (shown here as reference for how Angular should consume this API response)
// interface ValidationProblemDetails {
// type: string;
// title: string;
// status: number;
// traceId?: string;
// errors: { [key: string]: string[] };
// }
// In the Angular interceptor:
// if (error.status === 400 && error.error?.errors) {
// const validationErrors = error.error as ValidationProblemDetails;
// // Map field errors to form controls:
// Object.keys(validationErrors.errors).forEach(field => {
// const control = this.form.get(field);
// if (control) {
// control.setErrors({ serverError: validationErrors.errors[field][0] });
// }
// });
// }
// ── On the server: ensure field keys match Angular form control names ─────
// ASP.NET Core form key: "slug" (camelCase)
// Angular form control: this.form.get('slug') ← must match!
// If using [JsonPropertyName("post_slug")], the key would be "post_slug" — mismatch!
Common Mistakes
Mistake 1 — Inconsistent error shapes (string vs array vs object)
❌ Wrong — some endpoints return BadRequest("error"), others return ValidationProblem(ModelState); Angular needs two handlers.
✅ Correct — standardise on ValidationProblem(ModelState) for all field validation errors.
Mistake 2 — Returning 422 instead of 400 for validation failures
❌ Wrong — Angular’s error interceptor checking for status === 400 misses 422 validation errors.
✅ Correct — use 400 for all client input validation failures; 422 is rarely needed and adds complexity.