
In this article, we’ll take a look at how to integrate Pydantic with a Django application using the Pydantic-Django and Django Ninja packages.
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:
- Pydantic-Django – 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.
Pydantic-Django
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 Pydantic-Django, 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.9 -m venv env $ source env/bin/activate (env)$ pip install django==3.1.5 (env)$ django-admin.py 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 Pydantic-Django and Create the Schemas
Install Pydantic and Pydantic-Django:
(env)$ pip install pydantic==1.7.3 pydantic-django==0.0.7
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 pydantic_django 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']) schema = ArticleSchema.create( author=author, title=json_data['title'], content=json_data['content'] ) 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 at the import:
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.9 -m venv env $ source env/bin/activate (env)$ pip install django==3.1.5 (env)$ django-admin.py 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.10.1
Like with Pydantic-Django, you need to create schemas to validate your requests and responses. That said, auto-schema generation from Django models looks to be coming at some point.
For more on auto-schema generation support, review the Models to Schemas proposal.
Add the following to blog/schemas.py:
from datetime import datetime from ninja import Schema class UserSchema(Schema): id: int username: str class ArticleIn(Schema): author: int title: str content: str class ArticleOut(Schema): id: int author: UserSchema created: datetime title: str content: str
Here, we created 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 Pydantic-Django and Django Ninja.
Both packages are currently in active development. Just keep in mind that while both packages are immature, Pydantic-Django is still in an experimental phase (as of writing), so use it with caution.
You can grab the full code on GitHub:
all the time i used to read smaller content that also clear their motive, and
that is also happening with this paragraph which I
am reading here.
Appreciate the recommendation. Let me try it out.
Asking questions are actually nice thing if you are not understanding something totally, except
this piece of writing provides nice understanding yet.
I visited various web pages however the audio feature for audio songs current at this website is in fact fabulous.
The quality is very good, fluffy and supple, with the same color as my own hair. Satisfied