In this article, we'll take a look at how to integrate Pydantic with a Django application using the Djantic and Django Ninja packages.
Contents
Pydantic
Pydantic is a Python package for data validation and settings management that's based on Python type hints. It enforces type hints at runtime, provides user-friendly errors, allows custom data types, and works well with many popular IDEs. It's extremely fast and easy to use as well!
Let's look at an example:
from pydantic import BaseModel
class Song(BaseModel):
id: int
name: str
Here, we defined a Song
model with two attributes, both of which are required:
id
is an integername
is a string
Validation then happens on initialization:
>>> song = Song(id=1, name='I can almost see you')
>>> song.name
'I can almost see you'
>> Song(id='1')
pydantic.error_wrappers.ValidationError: 1 validation error for Song
name
field required (type=value_error.missing)
>>> Song(id='foo', name='I can almost see you')
pydantic.error_wrappers.ValidationError: 1 validation error for Song
id
value is not a valid integer (type=type_error.integer)
To learn more about Pydantic, be sure to read the Overview page from the official docs.
Pydantic and Django
When coupled with Django, we can use Pydantic to ensure that only data that matches the defined schemas are used in our application. So, we'll define schemas for validating requests and responses, and when a validation error occurs, we'll simply return a nice user-friendly error message.
While you can integrate Pydantic with Django without any third-party packages, we'll simplify the process by leveraging the following packages:
- Djantic - adds Pydantic support for validating model data
- Django Ninja - along with Pydantic, this package gives you a number of additional bells and whistles, like auto-generated API documentation (via OpenAPI and JSON Schema), serialization, and API versioning
Django Ninja is heavily inspired by FastAPI. Check it out if you like FastAPI but still what to leverage much of what Django has to offer.
Djantic
Now that you have a basic idea of what Pydantic is, let's take a look at a practical example. We'll create a simple RESTful API, with Django and Djantic, that allows us to fetch, list, and create articles.
Basic Setup
Start by setting up a new Django project:
$ mkdir django-with-pydantic && cd django-with-pydantic
$ python3.11 -m venv env
$ source env/bin/activate
(env)$ pip install django==4.2
(env)$ django-admin startproject core .
After that, create a new app called blog
:
(env)$ python manage.py startapp blog
Register the app in core/settings.py under INSTALLED_APPS
:
# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog.apps.BlogConfig', # new
]
Create Database Models
Next, let's create an Article
model.
Add the following to blog/models.py:
# blog/models.py
from django.contrib.auth.models import User
from django.db import models
class Article(models.Model):
author = models.ForeignKey(to=User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=512, unique=True)
content = models.TextField()
def __str__(self):
return f'{self.author.username}: {self.title}'
Create then apply the migrations:
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
Register the model in blog/admin.py so it's accessible from the Django admin panel:
# blog/admin.py
from django.contrib import admin
from blog.models import Article
admin.site.register(Article)
Install Djantic and Create the Schemas
Install Pydantic and Djantic:
(env)$ pip install pydantic==1.10.7 djantic==0.7.0
Now, we can define a schema, which will be used to-
- Validate the fields from a request payload, and then use the data to create new model objects
- Retrieve and validate model objects for response objects
Create a new file called blog/schemas.py:
# blog/schemas.py
from djantic import ModelSchema
from blog.models import Article
class ArticleSchema(ModelSchema):
class Config:
model = Article
This is the simplest possible schema, which derives from our model.
Django models need to be loaded before schemas, so the schemas must live in a separate file in order to avoid model loading errors.
With schemas, you can also define which fields should and shouldn't be included from a particular model by passing exclude
or include
to the Config
. For example, to exclude author
:
class ArticleSchema(ModelSchema):
class Config:
model = Article
exclude = ['author']
# or
class ArticleSchema(ModelSchema):
class Config:
model = Article
include = ['created', 'title', 'content']
You can also use schemas to override Django model properties by changing the fields inside the schema. For example:
class ArticleSchema(ModelSchema):
title: Optional[str]
class Config:
model = Article
Views and URLs
Next, let's set up the following endpoints:
/blog/articles/create/
creates a new article/blog/articles/<ARTICLE_ID>/
fetches a single article/blog/articles/
lists all articles
Add the following views to blog/views.py:
# blog/views.py
import json
from django.contrib.auth.models import User
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from blog.models import Article
from blog.schemas import ArticleSchema
@csrf_exempt # testing purposes; you should always pass your CSRF token with your POST requests (+ authentication)
@require_http_methods('POST')
def create_article(request):
try:
json_data = json.loads(request.body)
# fetch the user and pass it to schema
author = User.objects.get(id=json_data['author'])
article = Article(
author=author,
title=json_data['title'],
content=json_data['content']
)
article.save()
schema = ArticleSchema.from_django(article)
return JsonResponse({
'article': schema.dict()
})
except User.DoesNotExist:
return JsonResponse({'detail': 'Cannot find a user with this id.'}, status=404)
def get_article(request, article_id):
try:
article = Article.objects.get(id=article_id)
schema = ArticleSchema.from_django(article)
return JsonResponse({
'article': schema.dict()
})
except Article.DoesNotExist:
return JsonResponse({'detail': 'Cannot find an article with this id.'}, status=404)
def get_all_articles(request):
articles = Article.objects.all()
data = []
for article in articles:
schema = ArticleSchema.from_django(article)
data.append(schema.dict())
return JsonResponse({
'articles': data
})
Take note of the areas in which we're using the schema, ArticleSchema
:
ArticleSchema.create()
creates a newArticle
objectschema.dict()
returns a dictionary of the fields and values that we passed toJsonResponse
ArticleSchema.from_django()
generates a schema from anArticle
object
Remember: Both
create()
andfrom_django()
will also validate the data against the schema.
Add a urls.py file to "blog", and define the following URLs:
# blog/urls.py
from django.urls import path
from blog import views
urlpatterns = [
path('articles/create/', views.create_article),
path('articles/<str:article_id>/', views.get_article),
path('articles/', views.get_all_articles),
]
Now, let's register our app URLs to the base project:
# core/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include # new import
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')), # new
]
Sanity Check
To test, first create a superuser:
(env)$ python manage.py createsuperuser
Then, run the development server:
(env)$ python manage.py runserver
In a new terminal window, add a new article with cURL:
$ curl --header "Content-Type: application/json" --request POST \
--data '{"author":"1","title":"Something Interesting", "content":"Really interesting."}' \
http://localhost:8000/blog/articles/create/
You should see something like:
{
"article": {
"id": 1,
"author": 1,
"created": "2021-02-01T20:01:35.904Z",
"title": "Something Interesting",
"content": "Really interesting."
}
}
You should then able to view the article at http://127.0.0.1:8000/blog/articles/1/ and http://127.0.0.1:8000/blog/articles/.
Response Schema
Want to remove the created
field from the response for all articles?
Add a new schema to blog/schemas.py:
class ArticleResponseSchema(ModelSchema):
class Config:
model = Article
exclude = ['created']
Then, update the view:
def get_all_articles(request):
articles = Article.objects.all()
data = []
for article in articles:
schema = ArticleResponseSchema.from_django(article)
data.append(schema.dict())
return JsonResponse({
'articles': data
})
Don't forget to import ArticleResponseSchema
:
from blog.schemas import ArticleSchema, ArticleResponseSchema
Test it out at http://127.0.0.1:8000/blog/articles/.
Django Ninja
Django Ninja is a tool for building APIs with Django and Python-based type hints. As mentioned, it comes with a number of powerful features. It's "fast to learn, fast to code, fast to run".
Basic Setup
Create a new Django project:
$ mkdir django-with-ninja && cd django-with-ninja
$ python3.11 -m venv env
$ source env/bin/activate
(env)$ pip install django==4.2
(env)$ django-admin startproject core .
Create a new app called blog
:
(env)$ python manage.py startapp blog
Register the app in core/settings.py under INSTALLED_APPS
:
# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog.apps.BlogConfig', # new
]
Create Database Models
Next, add an Article
model to blog/models.py:
# blog/models.py
from django.contrib.auth.models import User
from django.db import models
class Article(models.Model):
author = models.ForeignKey(to=User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=512, unique=True)
content = models.TextField()
def __str__(self):
return f'{self.author.username}: {self.title}'
Create and apply the migrations:
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
Register the model in blog/admin.py:
# blog/admin.py
from django.contrib import admin
from blog.models import Article
admin.site.register(Article)
Install Django Ninja and Create the Schemas
Install:
(env)$ pip install django-ninja==0.21.0
Like with Djantic, you need to create schemas to validate your requests and responses. Django Ninja provides the flexibility to create schemas using both auto-schema generation and manual schema generation. Auto-schema generation allows you to automatically generate a schema based on your Django models, while manual schema generation gives you more control over the schema.
To create a schema for User
model using auto-schema generation, add the following to blog/schemas.py:
from django.contrib.auth.models import User
from ninja import ModelSchema
class UserSchema(ModelSchema):
class Config:
model = User
fields = ['id', 'username']
For more on auto-schema generation support, review Schemas from Django models.
To create the rest of the schemas manually, add the following to blog/schemas.py:
from datetime import datetime # new line
from django.contrib.auth.models import User
from ninja import ModelSchema, Schema # updated line
# Auto-schema generation for UserSchema
class UserSchema(ModelSchema):
class Config:
model = User
model_fields = ['id', 'username']
# Manual schema generation for ArticleIn and ArticleOut
class ArticleIn(Schema): # new schema
author: int
title: str
content: str
class ArticleOut(Schema): # new schema
id: int
author: UserSchema
created: datetime
title: str
content: str
Now we have three different schemas:
UserSchema
validates and converts data to/from the Django user modelArticleIn
validates and de-serializes data passed to the API for creating articlesArticleOut
validates and serializes data from theArticle
model
Django Ninja has a concept of a router, which is used to split up an API into multiple modules.
Create a blog/api.py file:
# blog/api.py
from typing import List
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from ninja import Router
from blog.models import Article
from blog.schemas import ArticleOut, ArticleIn
router = Router()
@router.post('/articles/create')
def create_article(request, payload: ArticleIn):
data = payload.dict()
try:
author = User.objects.get(id=data['author'])
del data['author']
article = Article.objects.create(author=author, **data)
return {
'detail': 'Article has been successfully created.',
'id': article.id,
}
except User.DoesNotExist:
return {'detail': 'The specific user cannot be found.'}
@router.get('/articles/{article_id}', response=ArticleOut)
def get_article(request, article_id: int):
article = get_object_or_404(Article, id=article_id)
return article
@router.get('/articles', response=List[ArticleOut])
def get_articles(request):
articles = Article.objects.all()
return articles
Here, we created three functions which serve as our views. Django Ninja uses HTTP operation decorators where you define the URL structure, path parameters, and optional request and response schemas.
Notes:
get_article
usesArticleOut
for it's response schema.ArticleOut
will thus be used to validate and serialize data from the model automatically.- In
get_articles
, the Django queryset -- e.g.,articles = Article.objects.all()
-- will be validated properly withList[ArticleOut]
.
Register API Endpoints
The last thing we have to do is to create a new instance of NinjaAPI
and register our API router in core/urls.py:
# core/urls.py
from django.contrib import admin
from django.urls import path
from ninja import NinjaAPI
from blog.api import router as blog_router
api = NinjaAPI()
api.add_router('/blog/', blog_router)
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', api.urls),
]
With that, Django Ninja will automatically create the following endpoints:
/blog/articles/create/
creates a new article/blog/articles/<ARTICLE_ID>/
fetches a single article/blog/articles/
lists all articles
Sanity Check
Create a superuser, and then run the development server:
(env)$ python manage.py createsuperuser
(env)$ python manage.py runserver
Navigate to http://localhost:8000/api/docs to view the auto-generated interactive API documentation:
Here, you can see and interact with the registered endpoints.
Try adding a new article:
Try the remaining endpoints on your own.
Conclusion
In this article, we first looked at what Pydantic is and then we looked at the how to integrate it with a Django application using Djantic and Django Ninja.
You can grab the full code on GitHub: