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 ninja 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 pass it as a response
argument to the @api.post
decorator:
from ninja import Schema
class UserIn(Schema):
username: str
password: str
class UserOut(Schema):
id: int
username: str
@api.post("/users/", response=UserOut)
def create_user(request, data: UserIn):
user = User(username=data.username)
user.set_password(data.password)
user.save()
return 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 List
from ninja import 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", response=List[TaskSchema])
def tasks(request):
queryset = Task.objects.select_related("owner")
return 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 ninja 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]
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()
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", response=List[TaskSchema])
def tasks(request):
return 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", response=List[TaskSchema])
async def tasks(request):
return 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 response schemas. In case of authentication, for example, you can return:
- 200 successful -> token
- 401 -> Unauthorized
- 402 -> Payment required
- etc..
In fact, the OpenAPI specification allows you to pass multiple response schemas.
You can pass to a response
argument a dictionary where:
- key is a response code
- value is a schema for that code
Also, when you return the result - you have to also pass a status code to tell Django Ninja which schema should be used for validation and serialization.
An example:
class Token(Schema):
token: str
expires: date
class Message(Schema):
message: str
@api.post('/login', response={200: Token, 401: Message, 402: Message})
def login(request, payload: Auth):
if auth_not_valid:
return 401, {'message': 'Unauthorized'}
if negative_balance:
return 402, {'message': 'Insufficient balance amount. Please proceed to a payment page.'}
return 200, {'token': xxx, ...}
Multiple response codes
In the previous example you saw that we basically repeated the Message
schema twice:
...401: Message, 402: Message}
To avoid this duplication you can use multiple response codes for a schema:
...
from ninja.responses import codes_4xx
@api.post('/login', response={200: Token, codes_4xx: Message})
def login(request, payload: Auth):
if auth_not_valid:
return 401, {'message': 'Unauthorized'}
if negative_balance:
return 402, {'message': 'Insufficient balance amount. Please proceed to a payment page.'}
return 200, {'token': xxx, ...}
Django Ninja comes with the following HTTP codes:
from ninja.responses import codes_1xx
from ninja.responses import codes_2xx
from ninja.responses import codes_3xx
from ninja.responses import codes_4xx
from ninja.responses import codes_5xx
You can also create your own range using a frozenset
:
my_codes = frozenset({416, 418, 425, 429, 451})
...
@api.post('/login', response={200: Token, my_codes: Message})
...
Empty responses
Some responses, such as 204 No Content
, have no body. To indicate the response body is empty mark response
argument with None
instead of Schema:
@api.post("/no_content", response={204: None})
def no_content(request):
return 204, None
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
update_forward_refs
method to apply self referencing types
class Organization(Schema):
title: str
part_of: 'Organization' = None #!! note the type in quotes here !!
Organization.update_forward_refs() # !!! this is important
@api.get('/organizations', response=List[Organization])
def list_organizations(request):
...
Self-referencing schemes from create_schema()
To be able to use the method update_forward_refs()
from a schema generated via create_schema()
,
the "name" of the class needs to be in our namespace. In this case it is very important to pass
the name
parameter to create_schema()
UserSchema = create_schema(
User,
name='UserSchema', # !!! this is important for update_forward_refs()
fields=['id', 'username']
custom_fields=[
('manager', 'UserSchema', None),
]
)
UserSchema.update_forward_refs()
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'}...]