In Django forms, where do validation errors end up, and how do form.errors, non_field_errors() and errors.as_json() differ?
answer
- a dictionary keyed by field
- a special key for the form
- codes survive in as_data()
- JSON for script-driven pages
- escaping is off by default
basics
~10 sDjango stores form errors in form.errors, a dict keyed by field name plus 'all' for form-wide errors. non_field_errors() returns that 'all' list, and errors.as_json() serialises every error as message and code.
solid answer
~40 s`form.errors` is an `ErrorDict`: each key is a field name, and each value is an `ErrorList` of messages. Errors raised from `Form.clean()`, or added with `add_error(None, ...)`, go under the special key `'__all__'` (the constant `NON_FIELD_ERRORS`), and `form.non_field_errors()` returns that list, or an empty one. Reading `form.errors` runs validation if it has not run yet. For code rather than templates, `form.errors.as_data()` returns the `ValidationError` objects with their `code`, `form.has_error('check_out', code='...')` answers a precise question in tests, and `as_json()` or `get_json_data()` give `{field: [{"message": ..., "code": ...}]}` for a script-driven page. Those JSON helpers do not HTML-escape messages unless you pass `escape_html=True`, so the client must insert them as text.
code
python · 14 linesfrom django.http import JsonResponse
from django.shortcuts import render
from .forms import BookingForm
def book_room(request):
form = BookingForm(request.POST)
if form.is_valid():
... # create the reservation
return JsonResponse({'ok': True})
# {'check_out': [{'message': 'Check-out must be after check-in.',
# 'code': 'checkout_before_checkin'}],
# '__all__': [{'message': '...', 'code': '...'}]}
return JsonResponse(form.errors.get_json_data(), status=400)go deeper
Recall that form.errors maps field names to messages, and that form-wide errors live under 'all' and come back from non_field_errors().
Explain as_data(), has_error() and get_json_data(), and when each is the right tool instead of the rendered messages.
Show you assert errors by code in tests, return structured JSON to script clients, and insert messages as text to avoid cross-site scripting.
Consider a consistent error contract across server-rendered pages and script clients so front ends can rely on stable codes.
## form.errors is a dictionary After validation, every problem Django found is in **`form.errors`**, an `ErrorDict` (a `dict` subclass that can render itself): - **Keys** are field names such as `'check_in'`, plus the special key `'__all__'` for errors that belong to the form as a whole. - **Values** are `ErrorList` objects, list-like collections of the messages for that key. - A field with no errors has no key at all, so `'guests' in form.errors` is a quick test. - Accessing `form.errors` on a bound form triggers `full_clean()` if it has not run; on an unbound form it is simply empty. `'__all__'` is exported as `django.core.exceptions.NON_FIELD_ERRORS`. Errors get there in two ways: raising `ValidationError` from `Form.clean()`, or calling `self.add_error(None, error)`. ## non_field_errors() `form.non_field_errors()` returns the `'__all__'` list, or an empty `ErrorList` when there is none, so templates can always iterate it. A hotel booking page uses it for "these dates overlap an existing reservation", while field errors appear next to their inputs. ## Keeping the codes: as_data() and has_error() The values in `form.errors` render as message strings, which vary with the active language and with custom `error_messages`. When code needs to know **which** error happened, use the structured forms: | API | Returns | Typical use | |---|---|---| | `form.errors` | `ErrorDict` of rendered messages | templates | | `form.non_field_errors()` | `ErrorList` for `'__all__'` | templates | | `form.errors.as_data()` | `{field: [ValidationError, ...]}` | view logic keyed on `code` | | `form.has_error(field, code=None)` | `bool` | tests and branching | | `form.errors.get_json_data()` | `{field: [{'message', 'code'}]}` | `JsonResponse` | | `form.errors.as_json()` | the same, as a JSON string | hand-built responses | `has_error('check_out', code='checkout_before_checkin')` is the robust way to assert an error in a test, because it does not break when someone rewords or translates the message. Pass `NON_FIELD_ERRORS` as the field to check form-wide errors. ## JSON for script-driven pages A booking widget that submits with JavaScript can return errors directly: 1. Validate the form in the view as usual. 2. On failure, return `JsonResponse(form.errors.get_json_data(), status=400)`. 3. In the page, show each `message` next to the input named by its key, and use `'__all__'` for a banner. The output carries only `message` and `code` for each error; `params` have already been interpolated into the message. The code is an empty string when the error was raised without one, which is a good reason to always pass `code=`. The JSON helpers **do not escape HTML** by default. If a message echoes user input ("'<script>' is not a valid promo code"), inserting it with `innerHTML` is a cross-site scripting hole. Insert messages as text on the client, or call `get_json_data(escape_html=True)` when the markup will be injected directly. ## Mistakes that show up in review - Serialising `str(form.errors)` into a JSON response: an `ErrorDict` renders as HTML, not data. - Custom templates that loop over fields and forget `non_field_errors()`, so form-wide errors never reach the user. - Branching on message text (`if 'after check-in' in ...`), which breaks on the first translation. - Expecting errors before validation: on an unbound form `form.errors` is empty, which is not the same as valid. ## What interviewers listen for - The shape of `form.errors` and the `'__all__'` key. - `non_field_errors()` for form-wide messages. - `as_data()` or `has_error()` whenever logic depends on which error occurred. - `get_json_data()` for script clients, with the escaping caveat.
- In a Django test, how do you assert that a form rejected the dates for the right reason?Call `form.has_error('check_out', code='checkout_before_checkin')`. It checks the error's `code`, not its text, so the test survives rewording and translation. For form-wide errors pass `NON_FIELD_ERRORS` from `django.core.exceptions` as the field name.
- Why might Django's form.errors.as_json() output be unsafe to insert into a page as-is?It does not HTML-escape messages by default, and messages can include user input through `params`. Insert them as text on the client, or pass `escape_html=True` so the messages arrive already escaped.
saying these in an interview costs you the question
- Form-wide errors are stored under a 'non_field_errors' key in form.errors.
- as_json() includes the params dictionary for every error.
- as_json() escapes HTML in messages by default.
- Tests should compare error message text to check the failure reason.
- Reading form.errors never triggers validation.