In a Django Ninja Schema, how does a resolve_<field> method compute an output field, and what constraints and costs come with it?
answer
- name after the resolve_ prefix
- static, receives the source object
- optional context argument
- runs once per serialised object
basics
~20 sDjango Ninja collects resolve_<field> methods when the Schema class is created and calls one instead of reading the attribute; in 1.7.1 it must be a @staticmethod taking the source object, optionally context, and it runs once per object.
solid answer
~40 sWhen a `Schema` class is created, Ninja's metaclass collects every callable named `resolve_<name>` (inherited ones included) and maps it to the field `<name>`. While validating, Ninja's getter checks that map before reading the attribute, so the resolver wins over the model attribute of the same name. The resolver receives the **source object** - the model instance or dict - and, if it declares a `context` parameter or `**kwargs`, the validation context, which for responses holds `request` and `response_status`. Its return value is then validated against the field's annotation. In 1.7.1 it must be a `@staticmethod`: a resolver taking `self` raises `NotImplementedError` when called, although the docs still show one. It runs **per object**, so touching `obj.room.name` on a list costs a query per row unless the view fetched the relation.
code
python · 27 linesfrom datetime import date
from typing import Optional
from ninja import Field, Schema
class BookingOut(Schema):
id: int
check_in: date
check_out: date
room_name: str = Field(..., alias="room.name")
nights: int
detail_url: Optional[str] = None
@staticmethod
def resolve_nights(obj):
return (obj.check_out - obj.check_in).days
@staticmethod
def resolve_detail_url(obj, context):
request = context["request"]
return request.build_absolute_uri(f"/api/bookings/{obj.id}")
@api.get("/bookings", response=list[BookingOut])
def list_bookings(request):
return Booking.objects.select_related("room")go deeper
Recall that resolve_<field> computes a field's value and that it must be a static method taking the source object.
Explain the lookup order: resolver first, then attribute or dotted alias, with the result validated against the annotation and context available on request.
Spot the per-object cost: resolvers touching relations or services multiply by page size, so the queryset in the view must load what they read.
Decide where presentation logic lives - resolvers, model properties or annotated querysets - so list endpoints stay cheap as schemas grow.
## What a resolver is In **Django Ninja**, a `Schema` field is normally filled by reading the attribute of the same name from the object being serialised. A **resolver** is a method named `resolve_<field>` that computes the value instead. It is the Ninja counterpart of a computed property, placed on the schema so the model does not need to know about API presentation. ## How Ninja wires it 1. When the class statement runs, Ninja's `ResolverMetaclass` scans the class namespace for callables whose name starts with `resolve_` and registers each under the name after the prefix; resolvers from base schemas are inherited. 2. During validation, Ninja wraps the source object in a getter. When Pydantic asks for a field, the getter looks in the resolver map **first**; only if there is no resolver does it read the attribute, a dict key, or a dotted path. 3. The resolver's return value goes back to Pydantic and is **validated against the field's annotation** like any attribute. The getter also post-processes plain attribute reads: a related `Manager` becomes a list, a callable such as `get_status_display` is called, and a `FieldFile` becomes its URL or `None`. ## The signature it must have | rule | detail | |---|---| | decorator | `@staticmethod` - in 1.7.1 a resolver taking `self` raises `NotImplementedError` ("Non static resolves are not supported yet") when called | | first argument | the **source object**: the model instance, dict or other object being validated, not the schema | | `context` | passed only if the resolver declares a `context` parameter or `**kwargs` | | return | any value the field's annotation accepts | The project docs still show a `def resolve_x(self, obj)` example that reads other validated fields through `self`; the 1.7.1 source does not support it and its test is skipped. Treat resolvers as static functions of the source object. During response serialisation the context contains `request` and `response_status`, so a resolver can build an absolute URL or hide a field from anonymous users. When the same schema validates a request body, the resolver runs there too, receiving the parsed data, and the context contains `request`. ## Resolver or alias For simple reshaping, a **dotted alias** is cheaper to read: `room_name: str = Field(None, alias="room.name")` walks the path with Django's template-variable lookup, and `status_label: str = Field(None, alias="get_status_display")` calls the method. A resolver is the right tool when there is logic: combining fields, formatting, branching on `None`, or reading the request from context. ## The cost: once per object A resolver runs for **every object** serialised. On a list endpoint returning 200 bookings, `resolve_room_name` reading `obj.room.name` triggers Django's lazy foreign-key load on each instance - 200 extra queries - unless the view's queryset already loaded the relation. The schema cannot fix this; the queryset in the view must. Other per-object costs to watch: - calling another service or cache inside a resolver multiplies by the page size; - an aggregate such as `obj.nights_booked()` that queries the database repeats per row; - in an async operation the resolver is still synchronous code touching the ORM. ## When a resolver is the wrong tool Some values are better computed by the database once than in Python per object. A count of a room's bookings, a price summed over line items, or a flag that depends on another table can be added to the queryset in the view as an annotation; the schema then declares a plain field with the same name and needs no resolver. Resolvers remain the right place for formatting and for values that need the request, such as absolute URLs or per-user visibility. A quick decision list: 1. A rename or a path into a related object - use a dotted alias. 2. Cheap formatting of fields already loaded - use a static resolver. 3. Data from other rows or tables - compute it in the view's queryset and declare a plain field. ## Mistakes interviewers probe - Writing a resolver with `self` because the docs show one, and getting `NotImplementedError` at runtime. - Expecting `self` to hold already-validated sibling fields; in 1.7.1 the resolver sees only the raw source object. - Declaring the field as `str` while the resolver can return `None`, turning a `None` into a validation failure and a 500. - Putting query logic in resolvers and wondering why the list endpoint is slow.
- If a Schema has both a room_name field with a dotted alias and a resolve_room_name method, which one wins?The alias wins. Pydantic reads an aliased field by its alias, so Ninja's getter looks up `room.name`, finds no resolver under that key and walks the dotted path; `resolve_room_name` is registered under `room_name` and never consulted. Use one mechanism per field.
- How can a resolver hide a field from anonymous users?Declare it as `Optional[...] = None` and give the static resolver a `context` parameter. During response serialisation `context["request"]` is the Django request, so the resolver can return `None` when `request.user` is not authenticated, and the operation can set `exclude_none=True` to drop the key.
A resolver is like a clerk filling one box on every form in a stack: each form gets the clerk's full attention, so if filling the box means walking to the archive, the walk happens once per form, not once per stack.
saying these in an interview costs you the question
- A resolver receives the validated schema instance as its first argument.
- Resolvers with self work in 1.7.1 exactly as the docs example shows.
- Ninja batches resolver calls into one query for the whole list.
- A resolver's return value skips validation against the field type.
- Resolvers only run on responses, never on request bodies.