skip to content

In Django forms, where do validation errors end up, and how do form.errors, non_field_errors() and errors.as_json() differ?

level: juniorimportance: should knowfreq 48%

answer

  1. a dictionary keyed by field
  2. a special key for the form
  3. codes survive in as_data()
  4. JSON for script-driven pages
  5. escaping is off by default

basics

~10 s

Django 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 lines
python
from 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

for a junior

Recall that form.errors maps field names to messages, and that form-wide errors live under 'all' and come back from non_field_errors().

for a middle

Explain as_data(), has_error() and get_json_data(), and when each is the right tool instead of the rendered messages.

for a senior

Show you assert errors by code in tests, return structured JSON to script clients, and insert messages as text to avoid cross-site scripting.

for a principal

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.