skip to content

In Django REST Framework, when do you use validate_<field_name>() versus validate(), and in what order does is_valid() run them?

level: middleimportance: must knowfreq 66%

answer

  1. one value versus the whole payload
  2. field errors stop the pipeline
  3. validators before validate()
  4. return the value, return attrs

basics

~20 s

validate_<field_name>(value) checks one field after its own conversion and validators; validate(attrs) checks rules across fields after every field passed and serializer-level validators ran. Both return the value, and a ValidationError in validate() lands under non_field_errors unless given a dict.

solid answer

~40 s

`is_valid()` runs fields first: each writable field converts its input, applies its validators, and then, if the serializer defines `validate_<field_name>(self, value)`, calls it with the converted value; it must return the value to keep. Errors from all fields are collected, and if any exist validation stops, so `validate()` never runs. Next come serializer-level validators, such as a `UniqueTogetherValidator`, then `validate(self, attrs)` with the dict of converted values, which must return it; returning `None` fails an assertion. I put single-value rules, like `guests >= 1`, in `validate_guests()`, and rules involving several fields, like `check_out > check_in` or `guests <= room.capacity`, in `validate()`. A `ValidationError("...")` raised in `validate()` appears under `non_field_errors`; raising it with a dict such as `{"check_out": "..."}` attaches the message to that field.

code

python · 27 lines
python
from django.utils import timezone
from rest_framework import serializers

from bookings.models import Booking


class BookingSerializer(serializers.ModelSerializer):
    class Meta:
        model = Booking
        fields = ["id", "room", "check_in", "check_out", "guests"]

    def validate_guests(self, value):
        if value < 1:
            raise serializers.ValidationError("At least one guest is required.")
        return value

    def validate_check_in(self, value):
        if value < timezone.localdate():
            raise serializers.ValidationError("Check-in cannot be in the past.")
        return value

    def validate(self, attrs):
        if attrs["check_out"] <= attrs["check_in"]:
            raise serializers.ValidationError({"check_out": "Check-out must be after check-in."})
        if attrs["guests"] > attrs["room"].capacity:
            raise serializers.ValidationError("Too many guests for this room.")  # non_field_errors
        return attrs

go deeper

for a junior

Know that validate_<field_name>() checks one field and validate() checks the whole payload, and that both must return what they validated.

for a middle

Explain the exact order, why validate() is skipped when any field fails, where serializer-level validators fit, and how error keys are chosen.

for a senior

Place rules deliberately between field hooks, reusable validators, validate() and database constraints, and keep non-ValidationError exceptions out of hooks.

for a principal

Keep business rules in one place that the API, forms and background jobs all call, instead of copies that drift between serializers and model clean().

## Two hooks, two scopes Django REST Framework gives a serializer two methods for custom rules: | Hook | Receives | Runs | Error lands under | |---|---|---|---| | `validate_<field_name>(self, value)` | one converted value | after that field's own conversion and validators | the field's name | | `validate(self, attrs)` | dict of all converted values | after every field passed and serializer-level validators ran | `non_field_errors`, or the keys of a dict you raise | Both must **return** what they received, optionally modified. A field method that forgets to return passes `None` onward as the field's value; `validate()` returning `None` fails an assertion (`.validate() should return the validated data`). ## The order is_valid() follows 1. **Per field**, for every writable field: - empty-value handling: a missing required field fails with `This field is required.`, a missing optional one is skipped or given its default, and `null` is accepted only with `allow_null=True`; - conversion to a Python value, for example parsing `"2026-10-03"` into a `date`; - the field's validators, including a `UniqueValidator` on a unique model field; - `validate_<field_name>(value)`, **only if the field produced a value**. A field that failed, or was skipped because it was absent and optional, never reaches it. 2. **Collect**: if any field failed, one `ValidationError` with every field's errors is raised and the pipeline stops. 3. **Serializer-level validators**: `Meta.validators`, or the uniqueness validators a `ModelSerializer` generates from the model's unique-together and unique constraints. 4. **`validate(attrs)`**, the last step, only reached when everything before it passed. The consequence clients notice: field errors and `validate()` errors **never appear together**. A booking with a malformed `check_in` gets only the field error; the date-range rule runs once the dates parse. ## Choosing the hook - **One value, no context**: `validate_guests()` rejects zero or negative guest counts; `validate_check_in()` rejects dates in the past. - **Several values**: `validate()` compares `check_in` with `check_out`, and checks `guests` against `room.capacity`, because it needs the room and the count together. - **Normalisation**: a field method may return a cleaned value, such as a trimmed and upper-cased promo code, and that returned value is what `validated_data` holds. - **A reusable rule**: a plain function or class passed in a field's `validators=[...]` list, so several serializers share it. - **Uniqueness across fields**: a `UniqueTogetherValidator`, generated or declared in `Meta.validators`, rather than a hand-written query in `validate()`. ## Field validators versus validate_<field_name>() Both check one value in the field phase, but they differ in reach: | Mechanism | Declared | Reusable across serializers | Can see the serializer | |---|---|---|---| | `validators=[...]` on a field | on the field, or through `extra_kwargs` | yes | only if the validator sets `requires_context = True` | | `validate_<field_name>()` | as a serializer method | no | yes, including `self.context` and `self.instance` | A method is the natural place when the rule needs the request, for example "only staff may book more than 30 days ahead", because `self.context["request"]` is available. A validator is better when the same rule applies in several serializers. A validator that sets `requires_context = True` is called with the field as a second argument, which is how DRF's own `UniqueValidator` reaches the instance being updated. ## Raising errors well - Raise `serializers.ValidationError`. A Django `django.core.exceptions.ValidationError` raised inside either hook is converted as well. - In `validate()`, raise with a **dict** to point at a field: `raise serializers.ValidationError({"check_out": "Check-out must be after check-in."})`. A plain string goes under `non_field_errors`, whose name comes from the `NON_FIELD_ERRORS_KEY` setting. - Do not let other exceptions escape: a `KeyError` or `AttributeError` inside a hook is not a validation error and becomes a server error. - Keep side effects out: hooks run before anything is saved, and a later hook can still reject the payload. ## What DRF does not run `is_valid()` does not call the model's `clean()` or `full_clean()`. A rule written in `Booking.clean()` is ignored by the API unless `validate()` calls it or repeats it, which is why teams that serve both a Django form and an API often move shared rules into functions both call.

  • Is validate_<field_name>() called in DRF when the field is absent from the payload?
    Not when it is skipped. A required field that is missing fails with `This field is required.` before the method runs; an optional field without a default, or any missing field under `partial=True`, is skipped and its method never runs. If the field has a default, the default goes through the method. Rules that must hold whether or not the field was sent belong in `validate()`.
  • How do you attach an error from DRF's validate() to a specific field?
    Raise `serializers.ValidationError` with a dict, such as `{"check_out": ["Check-out must be after check-in."]}`. DRF keeps the keys, so the error appears under `check_out` in `serializer.errors`. A string or list is treated as a non-field error and goes under `non_field_errors`, or whatever `NON_FIELD_ERRORS_KEY` is set to.

A booking desk checks each form box first: a date that is not a date is sent back before anyone looks at the whole form. Only a form whose boxes are all readable goes to the supervisor, who checks that the stay makes sense as a whole and that the room is big enough.

saying these in an interview costs you the question

  • validate() runs even when a field failed, so all errors come back at once.
  • validate_<field_name>() receives the raw string from the request.
  • validate() can return True to signal success.
  • Errors raised in validate() are keyed under __all__ as in Django forms.
  • DRF runs Model.clean() automatically after validate().