Response Schema
Django Ninja allows you to define the schema of your responses both for validation and documentation purposes.
Imagine you need to create an API operation that creates a user. The input parameter would be username+password, but output of this operation should be id+username (without the password).
Let's create the input schema:
from hattori import Schema
class UserIn(Schema):
username: str
password: str
@api.post("/users/")
def create_user(request, data: UserIn):
user = User(username=data.username) # User is django auth.User
user.set_password(data.password)
user.save()
# ... return ?
Now let's define the output schema, and use a return type annotation to declare the response:
from typing import Annotated
from hattori import Response, Schema
class UserIn(Schema):
username: str
password: str
class UserOut(Schema):
id: int
username: str
@api.post("/users/")
def create_user(request, data: UserIn) -> Annotated[Response[UserOut], 200]:
user = User(username=data.username)
user.set_password(data.password)
user.save()
return Response(200, user)
Django Ninja will use this response schema to:
- convert the output data to declared schema
- validate the data
- add an OpenAPI schema definition
- it will be used by the automatic documentation systems
- and, most importantly, it will limit the output data only to the fields only defined in the schema.
Nested objects
There is also often a need to return responses with some nested/child objects.
Imagine we have a Task Django model with a User ForeignKey:
from django.db import models
class Task(models.Model):
title = models.CharField(max_length=200)
is_completed = models.BooleanField(default=False)
owner = models.ForeignKey("auth.User", null=True, blank=True)
Now let's output all tasks, and for each task, output some fields about the user.
from typing import Annotated, List
from hattori import Response, Schema
class UserSchema(Schema):
id: int
first_name: str
last_name: str
class TaskSchema(Schema):
id: int
title: str
is_completed: bool
owner: UserSchema = None # ! None - to mark it as optional
@api.get("/tasks")
def tasks(request) -> Annotated[Response[List[TaskSchema]], 200]:
queryset = Task.objects.select_related("owner")
return Response(200, list(queryset))
If you execute this operation, you should get a response like this:
[
{
"id": 1,
"title": "Task 1",
"is_completed": false,
"owner": {
"id": 1,
"first_name": "John",
"last_name": "Doe",
}
},
{
"id": 2,
"title": "Task 2",
"is_completed": false,
"owner": null
},
]
Aliases
Instead of a nested response, you may want to just flatten the response output.
The Ninja Schema object extends Pydantic's Field(..., alias="") format to
work with dotted responses.
Using the models from above, let's make a schema that just includes the task
owner's first name inline, and also uses completed rather than is_completed:
from hattori import Field, Schema
class TaskSchema(Schema):
id: int
title: str
# The first Field param is the default, use ... for required fields.
completed: bool = Field(..., alias="is_completed")
owner_first_name: str = Field(None, alias="owner.first_name")
Aliases also support django template syntax variables access:
class TaskSchema(Schema):
last_message: str = Field(None, alias="message_set.0.text")
class TaskSchema(Schema):
type: str = Field(None)
type_display: str = Field(None, alias="get_type_display") # callable will be executed
Resolvers
You can also create calculated fields via resolve methods based on the field name.
The method must accept a single argument, which will be the object the schema is resolving against.
When creating a resolver as a standard method, self gives you access to other
validated and formatted attributes in the schema.
class TaskSchema(Schema):
id: int
title: str
is_completed: bool
owner: Optional[str] = None
lower_title: str
@staticmethod
def resolve_owner(obj):
if not obj.owner:
return
return f"{obj.owner.first_name} {obj.owner.last_name}"
def resolve_lower_title(self, obj):
return self.title.lower()
Accessing extra context
Pydantic v2 allows you to process an extra context that is passed to the serializer. In the following example you can have resolver that gets request object from passed context argument:
class Data(Schema):
a: int
path: str = ""
@staticmethod
def resolve_path(obj, context):
request = context["request"]
return request.path
if you use this schema for incoming requests - the request object will be automatically passed to context.
You can as well pass your own context:
data = Data.model_validate({'some': 1}, context={'request': MyRequest()})
Returning querysets
In the previous example we specifically converted a queryset into a list (and executed the SQL query during evaluation).
You can avoid that and return a queryset as a result, and it will be automatically evaluated to List:
@api.get("/tasks")
def tasks(request) -> Annotated[Response[List[TaskSchema]], 200]:
return Response(200, Task.objects.all())
Warning
If your operation is async, this example will not work because the ORM query needs to be called safely.
@api.get("/tasks")
async def tasks(request) -> Annotated[Response[List[TaskSchema]], 200]:
return Response(200, Task.objects.all())
See the async support guide for more information.
FileField and ImageField
Django Ninja by default converts files and images (declared with FileField or ImageField) to string URL's.
An example:
class Picture(models.Model):
title = models.CharField(max_length=100)
image = models.ImageField(upload_to='images')
If you need to output to response image field, declare a schema for it as follows:
class PictureSchema(Schema):
title: str
image: str
Once you output this to a response, the URL will be automatically generated for each object:
{
"title": "Zebra",
"image": "/static/images/zebra.jpg"
}
Multiple Response Schemas
Sometimes you need to define more than one response schema. In case of authentication, for example, you can return:
- 200 successful -> token
- 401 -> Unauthorized
- 402 -> Payment required
- 403 -> Forbidden
- etc..
In fact, the OpenAPI specification allows you to pass multiple response schemas.
You can use a union of Annotated[Response[...], code] types to declare multiple possible responses. When you return the result, use Response(status_code, body) to tell Django Ninja which schema should be used for validation and serialization.
An example:
from typing import Annotated
from hattori import Response, Schema
class Token(Schema):
token: str
expires: date
class Message(Schema):
message: str
@api.post('/login')
def login(request, payload: Auth) -> Annotated[Response[Token], 200] | Annotated[Response[Message], 401] | Annotated[Response[Message], 402]:
if auth_not_valid:
return Response(401, {'message': 'Unauthorized'})
if negative_balance:
return Response(402, {'message': 'Insufficient balance amount. Please proceed to a payment page.'})
return Response(200, {'token': xxx, ...})
Status code range matching
In the previous example you saw that we basically repeated the Message schema for both 401 and 402. You can simplify this by declaring the base status code of the range.
When you declare Annotated[Response[Message], 400], it automatically covers all 4xx codes (401, 402, etc. will fall back to 400's schema):
@api.post('/login')
def login(request, payload: Auth) -> Annotated[Response[Token], 200] | Annotated[Response[Message], 400]:
if auth_not_valid:
return Response(401, {'message': 'Unauthorized'})
if negative_balance:
return Response(402, {'message': 'Insufficient balance amount. Please proceed to a payment page.'})
return Response(200, {'token': xxx, ...})
Empty responses
Some responses, such as 204 No Content, have no body.
To indicate the response body is empty, use Response[None] as the schema type:
@api.post("/no_content")
def no_content(request) -> Annotated[Response[None], 204]:
return Response(204, None)
Error responses
Check Handling errors for more information.
Self-referencing schemes
Sometimes you need to create a schema that has reference to itself, or tree-structure objects.
To do that you need:
- set a type of your schema in quotes
- use
model_rebuildmethod to apply self referencing types
class Organization(Schema):
title: str
part_of: 'Organization' = None #!! note the type in quotes here !!
Organization.model_rebuild() # !!! this is important
@api.get('/organizations')
def list_organizations(request) -> Annotated[Response[List[Organization]], 200]:
...
Serializing Outside of Views
Serialization of your objects can be done directly in code through the use of
the .from_orm() method on the schema object.
Consider the following model:
class Person(models.Model):
name = models.CharField(max_length=50)
Which can be accessed using this schema:
class PersonSchema(Schema):
name: str
Direct serialization can be performed using the .from_orm() method on the
schema. Once you have an instance of the schema object, the .dict() and
.json() methods allow you to get at both dictionary output and string JSON
versions.
>>> person = Person.objects.get(id=1)
>>> data = PersonSchema.from_orm(person)
>>> data
PersonSchema(id=1, name='Mr. Smith')
>>> data.dict()
{'id':1, 'name':'Mr. Smith'}
>>> data.json()
'{"id":1, "name":"Mr. Smith"}'
Multiple Items: or a queryset (or list)
>>> persons = Person.objects.all()
>>> data = [PersonSchema.from_orm(i).dict() for i in persons]
[{'id':1, 'name':'Mr. Smith'},{'id': 2, 'name': 'Mrs. Smith'}...]
Django HTTP responses
It is also possible to return regular django http responses:
from django.http import HttpResponse
from django.shortcuts import redirect
@api.get("/http")
def result_django(request):
return HttpResponse('some data') # !!!!
@api.get("/something")
def some_redirect(request):
return redirect("/some-path") # !!!!