File: README.md
Django Ninja - Fast Django REST Framework
Django Ninja is a web framework for building APIs with Django and Python 3.6+ type hints.
Key features:
- Easy: Designed to be easy to use and intuitive.
- FAST execution: Very high performance thanks to Pydantic and async support.
- Fast to code: Type hints and automatic docs lets you focus only on business logic.
- Standards-based: Based on the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema.
- Django friendly: (obviously) has good integration with the Django core and ORM.
- Production ready: Used by multiple companies on live projects (If you use django-ninja and would like to publish your feedback, please email [email protected]).
Documentation: https://django-ninja.dev
Installation
pip install django-ninjaUsage
In your django project next to urls.py create new api.py file:
from ninja import NinjaAPI
api = NinjaAPI()
@api.get("/add")
def add(request, a: int, b: int):
return {"result": a + b}Now go to urls.py and add the following:
...
from .api import api
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", api.urls), # <---------- !
]That's it !
Now you've just created an API that:
- receives an HTTP GET request at
/api/add - takes, validates and type-casts GET parameters
aandb - decodes the result to JSON
- generates an OpenAPI schema for defined operation
Interactive API docs
Now go to http://127.0.0.1:8000/api/docs
You will see the automatic interactive API documentation (provided by Swagger UI or Redoc):
Sponsors
What next?
- Read the full documentation here - https://django-ninja.dev
- To support this project, please give star it on Github.
- Share it via Twitter
- If you already using django-ninja, please share your feedback to [email protected]
File: docs/docs/guides/input/body.md
Request Body
Request bodies are typically used with โcreateโ and โupdateโ operations (POST, PUT, PATCH).
For example, when creating a resource using POST or PUT, the request body usually contains the representation of the resource to be created.
To declare a request body, you need to use Django Ninja Schema.
!!! info
Under the hood Django Ninja uses Pydantic models with all their power and benefits.
The alias Schema was chosen to avoid confusion in code when using Django models, as Pydantic's model class is called Model by default, and conflicts with Django's Model class.
Import Schema
First, you need to import Schema from ninja:
{!./src/tutorial/body/code01.py!}Create your data model
Then you declare your data model as a class that inherits from Schema.
Use standard Python types for all the attributes:
{!./src/tutorial/body/code01.py!}Note: if you use None as the default value for an attribute, it will become optional in the request body.
For example, this model above declares a JSON "object" (or Python dict) like:
{
"name": "Katana",
"description": "An optional description",
"price": 299.00,
"quantity": 10
}...as description is optional (with a default value of None), this JSON "object" would also be valid:
{
"name": "Katana",
"price": 299.00,
"quantity": 10
}Declare it as a parameter
To add it to your path operation, declare it the same way you declared the path and query parameters:
{!./src/tutorial/body/code01.py!}... and declare its type as the model you created, Item.
Results
With just that Python type declaration, Django Ninja will:
- Read the body of the request as JSON.
- Convert the corresponding types (if needed).
- Validate the data.
- If the data is invalid, it will return a nice and meaningful error, indicating exactly where and what the incorrect data was.
- Give you the received data in the parameter
item.- Because you declared it in the function to be of type
Item, you will also have all the editor support
(completion, etc.) for all the attributes and their types.
- Because you declared it in the function to be of type
- Generate JSON Schema definitions for
your models, and you can also use them anywhere else you like if it makes sense for your project. - Those schemas will be part of the generated OpenAPI schema, and used by the automatic documentation UI's.
Automatic docs
The JSON Schemas of your models will be part of your OpenAPI generated schema, and will be shown in the interactive API docs:
... and they will be also used in the API docs inside each path operation that needs them:
Editor support
In your editor, inside your function you will get type hints and completion everywhere (this wouldn't happen if you received a dict instead of a Schema object):
The previous screenshots were taken with Visual Studio Code.
You would get the same editor support with PyCharm and most of the other Python editors.
Request body + path parameters
You can declare path parameters and body requests at the same time.
Django Ninja will recognize that the function parameters that match path parameters should be taken from the path, and that function parameters that are declared with Schema should be taken from the request body.
{!./src/tutorial/body/code02.py!}Request body + path + query parameters
You can also declare body, path and query parameters, all at the same time.
Django Ninja will recognize each of them and take the data from the correct place.
{!./src/tutorial/body/code03.py!}The function parameters will be recognized as follows:
- If the parameter is also declared in the path, it will be used as a path parameter.
- If the parameter is of a singular type (like
int,float,str,bool, etc.), it will be interpreted as a query parameter. - If the parameter is declared to be of the type of Schema (or Pydantic
BaseModel), it will be interpreted as a request body.
File: docs/docs/guides/input/file-params.md
File uploads
Handling files are no different from other parameters.
from ninja import NinjaAPI, File
from ninja.files import UploadedFile
@api.post("/upload")
def upload(request, file: File[UploadedFile]):
data = file.read()
return {'name': file.name, 'len': len(data)}UploadedFile is an alias to Django's UploadFile and has all the methods and attributes to access the uploaded file:
- read()
- multiple_chunks(chunk_size=None)
- chunks(chunk_size=None)
- name
- size
- content_type
- content_type_extra
- charset
- etc.
Uploading array of files
To upload several files at the same time, just declare a List of UploadedFile:
from typing import List
from ninja import NinjaAPI, File
from ninja.files import UploadedFile
@api.post("/upload-many")
def upload_many(request, files: File[List[UploadedFile]]):
return [f.name for f in files]Uploading files with extra fields
Note: The HTTP protocol does not allow you to send files in application/json format by default (unless you encode it somehow to JSON on client side)
To send files along with some extra attributes, you need to send bodies with multipart/form-data encoding. You can do it by simply marking fields with Form:
from ninja import NinjaAPI, Schema, UploadedFile, Form, File
from datetime import date
api = NinjaAPI()
class UserDetails(Schema):
first_name: str
last_name: str
birthdate: date
@api.post('/users')
def create_user(request, details: Form[UserDetails], file: File[UploadedFile]):
return [details.dict(), file.name]Note: in this case all fields should be send as form fields
You can as well send payload in single field as JSON - just remove the Form mark from:
@api.post('/users')
def create_user(request, details: UserDetails, file: File[UploadedFile]):
return [details.dict(), file.name]this will expect from the client side to send data as `multipart/form-data with 2 fields:
- details: JSON as string
- file: file
List of files with extra info
@api.post('/users')
def create_user(request, details: Form[UserDetails], files: File[list[UploadedFile]]):
return [details.dict(), [f.name for f in files]]Optional file input
If you would like the file input to be optional, all that you have to do is to pass None to the File type, like so:
@api.post('/users')
def create_user(request, details: Form[UserDetails], avatar: File[UploadedFile] = None):
user = add_user_to_database(details)
if avatar is not None:
set_user_avatar(user)Handling request.FILES in PUT/PATCH Requests
Problem
@api.put("/upload") # !!!!
def upload(request, file: File[UploadedFile]):
...For some historical reasons Djangoโs request.FILES is populated only for POST requests by default. When using HTTP PUT or PATCH methods with file uploads (e.g., multipart/form-data), request.FILES will not contain uploaded files. This is a known Django behavior, not specific to Django Ninja.
As a result, views expecting files in PUT or PATCH requests may not behave correctly, since request.FILES will be empty.
Solution
Django Ninja provides a built-in middleware to automatically fix this behavior:ninja.compatibility.files.fix_request_files_middleware
This middleware will manually parse multipart/form-data for PUT and PATCH requests and populate request.FILES, making file uploads work as expected across all HTTP methods.
Usage
To enable the middleware, add the following to your Django settings:
MIDDLEWARE = [
# ... your existing middleware ...
"ninja.compatibility.files.fix_request_files_middleware",
]Auto-detection
When Django Ninja detects a PUT or PATCH etc methods with multipart/form-data and expected FILES - it will throw an error message suggesting you install the compatibility middleware:
Note: This middleware does not interfere with normal POST behavior or any other methods.
File: docs/docs/guides/input/filtering.md
Filtering
If you want to allow the user to filter your querysets by a number of different attributes, it makes sense
to encapsulate your filters into a FilterSchema class. FilterSchema is a regular Schema, so it's using all the
necessary features of Pydantic, but it also adds some bells and whistles that ease the translation of the user-facing filtering
parameters into database queries.
Start off with defining a subclass of FilterSchema:
from ninja import FilterSchema
from typing import Optional
from datetime import datetime
class BookFilterSchema(FilterSchema):
name: Optional[str] = None
author: Optional[str] = None
created_after: Optional[datetime] = NoneNext, use this schema in conjunction with Query in your API handler:
@api.get("/books")
def list_books(request, filters: BookFilterSchema = Query(...)):
books = Book.objects.all()
books = filters.filter(books)
return booksJust like described in defining query params using schema, Django Ninja converts the fields
defined in BookFilterSchema into query parameters.
You can use a shorthand one-liner .filter() to apply those filters to your queryset:
@api.get("/books")
def list_books(request, filters: Query[BookFilterSchema]):
books = Book.objects.all()
books = filters.filter(books)
return booksUnder the hood, FilterSchema converts its fields into Q expressions which it then combines and uses to filter your queryset.
Alternatively to using the .filter method, you can get the prepared Q-expression and perform the filtering yourself.
That can be useful, when you have some additional queryset filtering on top of what you expose to the user through the API:
@api.get("/books")
def list_books(request, filters: Query[BookFilterSchema]):
# Never serve books from inactive publishers and authors
q = Q(author__is_active=True) | Q(publisher__is_active=True)
# But allow filtering the rest of the books
q &= filters.get_filter_expression()
return Book.objects.filter(q)By default, the filters will behave the following way:
Nonevalues will be ignored and not filtered against;- Every non-
Nonefield will be converted into aQ-expression based on theFielddefinition of each field; - All
Q-expressions will be merged into one usingANDlogical operator; - The resulting
Q-expression is used to filter the queryset and return you a queryset with a.filterclause applied.
Customizing Fields
By default, FilterSet will use the field names to generate Q expressions:
class BookFilterSchema(FilterSchema):
name: Optional[str] = NoneThe name field will be converted into Q(name=...) expression.
When your database lookups are more complicated than that, you can annotate your fields with an instance of FilterLookup where you specify how you wish your field to be looked up for filtering:
from ninja import FilterSchema, FilterLookup
from typing import Annotated
class BookFilterSchema(FilterSchema):
name: Annotated[Optional[str], FilterLookup("name__icontains")] = NoneYou can even specify multiple lookups as a list:
class BookFilterSchema(FilterSchema):
search: Annotated[Optional[str], FilterLookup(
["name__icontains",
"author__name__icontains",
"publisher__name__icontains"]
)]By default, field-level expressions are combined using "OR" connector, so with the above setup, a query parameter ?search=foobar will search for books that have "foobar" in either of their name, author or publisher.
And to make generic fields, you can make the field name implicit by skipping it:
IContainsField = Annotated[Optional[str], FilterLookup('__icontains')]
class BookFilterSchema(FilterSchema):
name: IContainsField = None??? note "Deprecated syntax"
In previous versions, database lookups were specified using `Field(q=...)` syntax:
```python
from ninja import FilterSchema, Field
class BookFilterSchema(FilterSchema):
name: Optional[str] = Field(None, q="name__icontains")
```
This approach is still supported, but it is considered **deprecated** and **not recommended** for new code because:
- Poor IDE support (IDEs don't recognize custom `Field` arguments)
- Uses deprecated Pydantic features (`**extra`)
- Less type-safe and harder to maintain
The new `FilterLookup` annotation provides better developer experience with full IDE support and type safety. Prefer using `FilterLookup` for new projects.Combining expressions
By default,
- Field-level expressions are joined together using
ORoperator. - The fields themselves are joined together using
ANDoperator.
So, with the following FilterSchema...
class BookFilterSchema(FilterSchema):
search: Annotated[
Optional[str],
FilterLookup(["name__icontains", "author__name__icontains"])] = None
popular: Optional[bool] = None...and the following query parameters from the user
http://localhost:8000/api/books?search=harry&popular=truethe FilterSchema instance will look for popular books that have harry in the book's or author's name.
You can customize this behavior using an expression_connector argument in field-level and class-level definition:
from ninja import FilterConfigDict, FilterLookup, FilterSchema
class BookFilterSchema(FilterSchema):
active: Annotated[
Optional[bool],
FilterLookup(
["is_active", "publisher__is_active"],
expression_connector="AND"
)] = None
name: Annotated[Optional[str], FilterLookup("name__icontains")] = None
model_config = FilterConfigDict(expression_connector="OR")An expression connector can take the values of "OR", "AND" and "XOR", but the latter is only supported in Django starting with 4.1.
Now, a request with these query parameters
http://localhost:8000/api/books?name=harry&active=true...shall search for books that have harry in their name or are active themselves and are published by active publishers.
Filtering by Nones
You can make the FilterSchema treat None as a valid value that should be filtered against.
This can be done on a field level with a ignore_none kwarg:
class BookFilterSchema(FilterSchema):
name: Annotated[Optional[str], FilterLookup("name__icontains")] = None
tag: Annotated[Optional[str], FilterLookup("tag", ignore_none=False)] = NoneThis way when no other value for "tag" is provided by the user, the filtering will always include a condition tag=None.
You can also specify this setting for all fields at the same time in model_config:
class BookFilterSchema(FilterSchema):
name: Annotated[Optional[str], FilterLookup("name__icontains")] = None
tag: Optional[str] = None
model_config = FilterConfigDict(ignore_none=False)Custom expressions
Sometimes you might want to have complex filtering scenarios that cannot be handled by individual Field annotations.
For such cases you can implement your field filtering logic as a custom method. Simply define a method called filter_<fieldname> which takes a filter value and returns a Q expression:
class BookFilterSchema(FilterSchema):
tag: Optional[str] = None
popular: Optional[bool] = None
def filter_popular(self, value: bool) -> Q:
return Q(view_count__gt=1000) | Q(download_count__gt=100) if value else Q()Such field methods take precedence over what is specified in the Field() definition of the corresponding fields.
If that is not enough, you can implement your own custom filtering logic for the entire FilterSet class in a custom_expression method:
class BookFilterSchema(FilterSchema):
name: Optional[str] = None
popular: Optional[bool] = None
def custom_expression(self) -> Q:
q = Q()
if self.name:
q &= Q(name__icontains=self.name)
if self.popular:
q &= (
Q(view_count__gt=1000) |
Q(downloads__gt=100) |
Q(tag='popular')
)
return qThe custom_expression method takes precedence over any other definitions described earlier, including filter_<fieldname> methods.
File: docs/docs/guides/input/form-params.md
Form data
Django Ninja also allows you to parse and validate request.POST data
(aka application/x-www-form-urlencoded or multipart/form-data).
Form Data as params
from ninja import NinjaAPI, Form
@api.post("/login")
def login(request, username: Form[str], password: Form[str]):
return {'username': username, 'password': '*****'}Note the following:
- You need to import the
Formclass fromninja
from ninja import Form- Use
Formas default value for your parameter:
username: Form[str]Using a Schema
In a similar manner to Body, you can use
a Schema to organize your parameters.
{!./src/tutorial/form/code01.py!}Request form + path + query parameters
In a similar manner to Body, you can use
Form data in combination with other parameter sources.
You can declare query and path and form field, and etc... parameters at the same time.
Django Ninja will recognize that the function parameters that match path
parameters should be taken from the path, and that function parameters that
are declared with Form(...) should be taken from the request form fields, etc.
{!./src/tutorial/form/code02.py!}Mapping Empty Form Field to Default
Form fields that are optional, are often sent with an empty value. This value is
interpreted as an empty string, and thus may fail validation for fields such as int or bool.
This can be fixed, as described in the Pydantic docs, by using
Generic Classes as Types.
{!./src/tutorial/form/code03.py!}File: docs/docs/guides/input/operations.md
HTTP Methods
Defining operations
An operation can be one of the following HTTP methods:
- GET
- POST
- PUT
- DELETE
- PATCH
Django Ninja comes with a decorator for each operation:
@api.get("/path")
def get_operation(request):
...
@api.post("/path")
def post_operation(request):
...
@api.put("/path")
def put_operation(request):
...
@api.delete("/path")
def delete_operation(request):
...
@api.patch("/path")
def patch_operation(request):
...See the operations parameters
reference docs for information on what you can pass to any of these decorators.
Handling multiple methods
If you need to handle multiple methods with a single function for a given path,
you can use the api_operation decorator:
@api.api_operation(["POST", "PATCH"], "/path")
def mixed_operation(request):
...This feature can also be used to implement other HTTP methods that don't have
corresponding Django Ninja methods, such as HEAD or OPTIONS.
@api.api_operation(["HEAD", "OPTIONS"], "/path")
def mixed_operation(request):
...File: docs/docs/guides/input/path-params.md
Path parameters
You can declare path "parameters" with the same syntax used by Python format-strings (which luckily also matches the OpenAPI path parameters):
{!./src/tutorial/path/code01.py!}The value of the path parameter item_id will be passed to your function as the argument item_id.
So, if you run this example and go to http://localhost:8000/api/items/foo, you will see this response:
{"item_id":"foo"}Path parameters with types
You can declare the type of path parameter in the function using standard Python type annotations:
{!./src/tutorial/path/code02.py!}In this case,item_id is declared to be an int. This will give you editor and linter support for error checks, completion, etc.
If you run this in your browser with http://localhost:8000/api/items/3, you will see this response:
{"item_id":3}!!! tip
Notice that the value your function received (and returned) is 3, as a Python int - not a string "3".
So, with just that type declaration, Django Ninja gives you automatic request "parsing" and validation.
Data validation
On the other hand, if you go to the browser at http://localhost:8000/api/items/foo ("foo" is not int), you will see an HTTP error like this:
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}Django Path Converters
You can use Django Path Converters
to help parse the path:
@api.get("/items/{int:item_id}")
def read_item(request, item_id):
return {"item_id": item_id}In this case,item_id will be parsed as an int. If item_id is not a valid int, the url will not
match. (e.g. if no other path matches, a 404 Not Found will be returned)
!!! tip
Notice that, since Django Ninja uses a default type of str for unannotated parameters, the value the
function above received (and returned) is "3", as a Python str - not an integer 3. To receive
an int, simply declare item_id as an int type annotation in the function definition as normal:
```python hl_lines="2"
@api.get("/items/{int:item_id}")
def read_item(request, item_id:int):
return {"item_id": item_id}
```Path params with slashes
Django's path converter allows you to handle path-like parameters:
@api.get('/dir/{path:value}')
def someview(request, value: str):
return valueyou can query this operation with /dir/some/path/with-slashes and your value will be equal to some/path/with-slashes
Multiple parameters
You can pass as many variables as you want into path, just remember to have unique names and don't forget to use the same names in the function arguments.
@api.get("/events/{year}/{month}/{day}")
def events(request, year: int, month: int, day: int):
return {"date": [year, month, day]}Using Schema
You can also use Schema to encapsulate path parameters that depend on each other (and validate them as a group):
{!./src/tutorial/path/code010.py!}!!! note
Notice that here we used a Path source hint to let Django Ninja know that this schema will be applied to path parameters.
Documentation
Now, when you open your browser at http://localhost:8000/api/docs, you will see the automatic, interactive, API documentation.
File: docs/docs/guides/input/query-params.md
Query parameters
When you declare other function parameters that are not part of the path parameters, they are automatically interpreted as "query" parameters.
{!./src/tutorial/query/code01.py!}To query this operation, you use a URL like:
http://localhost:8000/api/weapons?offset=0&limit=10By default, all GET parameters are strings, and when you annotate your function arguments with types, they are converted to that type and validated against it.
The same benefits that apply to path parameters also apply to query parameters:
- Editor support (obviously)
- Data "parsing"
- Data validation
- Automatic documentation
!!! Note
if you do not annotate your arguments, they will be treated as str types
@api.get("/weapons")
def list_weapons(request, limit, offset):
# type(limit) == str
# type(offset) == strDefaults
As query parameters are not a fixed part of a path, they are optional and can have default values:
@api.get("/weapons")
def list_weapons(request, limit: int = 10, offset: int = 0):
return weapons[offset : offset + limit]In the example above we set default values of offset=0 and limit=10.
So, going to the URL:
http://localhost:8000/api/weaponswould be the same as going to:
http://localhost:8000/api/weapons?offset=0&limit=10If you go to, for example:
http://localhost:8000/api/weapons?offset=20the parameter values in your function will be:
offset=20(because you set it in the URL)limit=10(because that was the default value)
Required and optional parameters
You can declare required or optional GET parameters in the same way as declaring Python function arguments:
{!./src/tutorial/query/code02.py!}In this case, Django Ninja will always validate that you pass the q param in the GET, and the offset param is an optional integer.
GET parameters type conversion
Let's declare multiple type arguments:
{!./src/tutorial/query/code03.py!}The str type is passed as is.
For the bool type, all the following:
http://localhost:8000/api/example?b=1
http://localhost:8000/api/example?b=True
http://localhost:8000/api/example?b=true
http://localhost:8000/api/example?b=on
http://localhost:8000/api/example?b=yesor any other case variation (uppercase, first letter in uppercase, etc.), your function will see
the parameter b with a bool value of True, otherwise as False.
Date can be both date string and integer (unix timestamp):
http://localhost:8000/api/example?d=**1577836800** # same as 2020-01-01
http://localhost:8000/api/example?d=**2020-01-01**Using Schema
You can also use Schema to encapsulate GET parameters:
{!./src/tutorial/query/code010.py!}For more complex filtering scenarios please refer to filtering.
File: docs/docs/guides/input/request-parsers.md
Request parsers
In most cases, the default content type for REST API's is JSON, but in case you need to work with
other content types (like YAML, XML, CSV) or use faster JSON parsers, Django Ninja provides a parser configuration.
api = NinjaAPI(parser=MyYamlParser())To create your own parser, you need to extend the ninja.parser.Parser class, and override the parse_body method.
Example YAML Parser
Let's create our custom YAML parser:
import yaml
from typing import List
from ninja import NinjaAPI
from ninja.parser import Parser
class MyYamlParser(Parser):
def parse_body(self, request):
return yaml.safe_load(request.body)
api = NinjaAPI(parser=MyYamlParser())
class Payload(Schema):
ints: List[int]
string: str
f: float
@api.post('/yaml')
def operation(request, payload: Payload):
return payload.dict()If you now send YAML like this as the request body:
ints:
- 0
- 1
string: hello
f: 3.14it will be correctly parsed, and you should have JSON output like this:
{
"ints": [
0,
1
],
"string": "hello",
"f": 3.14
}Example ORJSON Parser
orjson is a fast, accurate JSON library for Python. It benchmarks as the fastest Python library for JSON and is more accurate than the standard json library or other third-party libraries.
pip install orjsonParser code:
import orjson
from ninja import NinjaAPI
from ninja.parser import Parser
class ORJSONParser(Parser):
def parse_body(self, request):
return orjson.loads(request.body)
api = NinjaAPI(parser=ORJSONParser())File: docs/docs/guides/response/config-pydantic.md
Overriding Pydantic Config
There are many customizations available for a Django Ninja Schema, via the schema's
Pydantic model_config.
!!! info
Under the hood Django Ninja uses Pydantic Models
with all their power and benefits. The alias Schema was chosen to avoid confusion in code
when using Django models, as Pydantic's model class is called Model by default, and conflicts with
Django's Model class.
Example Camel Case mode
One interesting config attribute is alias_generator.
Using Pydantic's example in Django Ninja can look something like:
from pydantic import ConfigDict
from ninja import Schema
def to_camel(string: str) -> str:
words = string.split('_')
return words[0].lower() + ''.join(word.capitalize() for word in words[1:])
class CamelModelSchema(Schema):
model_config = ConfigDict(alias_generator=to_camel)
str_field_name: str
float_field_name: floatKeep in mind that when you want modify output for field names (like camel case) - you need to set as well populate_by_name and by_alias
from pydantic import ConfigDict
class UserSchema(ModelSchema):
model_config = ConfigDict(
alias_generator = to_camel
populate_by_name = True, # !!!!!! <--------
)
class Meta:
model = User
fields = ["id", "email", "is_staff"]
@api.get("/users", response=list[UserSchema], by_alias=True) # !!!!!! <-------- by_alias
def get_users(request):
return User.objects.all()results:
[
{
"id": 1,
"email": "[email protected]",
"isStaff": true
},
{
"id": 2,
"email": "[email protected]",
"isStaff": false
}
...
]