Integrate Pydantic with Django and Django REST Framework

Yasser Tahiri
December 15th, 2021 · 3 min read

Python has always been a dynamically typed language, which means you don’t have to specify data types for variables and function return values. PEP 484 introduced type hints — a way to make Python feel statically typed.

While type hints can help structure your projects better, they are just that — hints — and by default do not affect the runtime. However, there is a way to force type checks on runtime, and we’ll explore it today, after dialing in on some basics.

The function below takes and returns a string and is annotated as follows:

1def username(name: str) -> str:
2 return 'Hello' + name

In the function username, the argument name is expected to be of type str and the return type str. Subtypes are accepted as arguments.

In this article we’ll explore how to use Pydantic with Django and Django REST Framework, and how to force type checks on runtime.

Bimo_Dance

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:

1from pydantic import BaseModel
2
3class User(BaseModel):
4 id: int
5 name = str
6
7external_data = {
8 'id': '1',
9 'name': 'John Doe',
10}
11user = User(**external_data)

Here we’re creating a User model that has an id field of type int and a name field of type str.

1print(user.id)
2>>> 1
3print(user.name)
4>>> John Doe

To learn more about Pydantic, be sure to read the Overview page from the official docs.

Pydantic with Django

When coupled with Django, we can use Pydantic to ensure that only data that matches the defined schemas are used in our application.

That’s why we will use Pyngo an open source Python package help to integrate Pydantic with Django, and Django REST Framework, and give us multiple parameters to use with OpenAPI.

Born_to_do_this

Getting Started

To get started, we need to install Pyngo, as we know it’s based on Pydantic, Django, and typing_extensions.

just run the following command:

1$ pip install pyngo

Features

  • Using Pydantic to Build your Models in Django Project.
  • Using OpenAPI utilities to build params from a basic model.
  • Using QueryDictModel to build Pydantic models from a QueryDict object.
  • Propagate any errors from Pydantic in Django Rest Framework.

OpenAPI parameters

pyngo.openapi_params() Build a List of ParameterDict describing the fields of a pydantic.BaseModel. By setting the fields according to the dictionary we have it before in ParameterDict class.

Mostly this dictionary representing the fixed fields of an OpenAPI parameter object.

As variables we can use the following fields:

  • name (str) – The name of the parameter. Parameter names are case sensitive.
  • in (Literal["query", "header", "path", "cookie"]) – The location of the parameter.
  • description (str) – A brief description of the parameter. This could contain examples of use.
  • required (bool) – Determines whether or not this parameter is required or optional.
  • deprecated (bool) – Determines whether or not the parameter is in the process of being removed from the specification.
  • allowEmptyValue (bool) – Determines whether or not the parameter value can be omitted (null or an empty string).

openapi_params()

pyngo.openapi_params() can build params from a basic model:

1from pydantic import BaseModel
2from pyngo import openapi_params
3
4class Model(BaseModel):
5 id: int
6
7print(openapi_params(Model))

we will see that the response in json format, and id will be the name of the parameter, this is what we got in a basic model:

1[
2 {
3 'name': 'id',
4 'in': 'query',
5 'description': '',
6 'required': True,
7 'deprecated': False,
8 'allowEmptyValue': False
9 }
10]

Let’s try to extand using pyngo.ParameterDict.required is set according to the type of the variable:

1from typing import Optional
2from pydantic import BaseModel
3from pyngo import openapi_params
4
5class Model(BaseModel):
6 required_param: int
7 optional_param: Optional[int]
8
9print(openapi_params(Model))

same result but we will see the changes in the required field:

1[
2 {
3 'allowEmptyValue': False,
4 'deprecated': False,
5 'description': '',
6 'in': 'query',
7 'name': 'required_param',
8 'required': True
9 },
10 {
11 'allowEmptyValue': False,
12 'deprecated': False,
13 'description': '',
14 'in': 'query',
15 'name': 'optional_param',
16 'required': False
17 }
18]

We can also setup the other fields of the ParameterDict class:

1from pydantic import BaseModel, Field
2from pyngo import openapi_params
3
4class WithDescription(BaseModel):
5 described_param: str = Field(
6 description="Hello World Use Me!"
7 )
8
9class InPath(BaseModel):
10 path_param: str = Field(location="path")
11
12class WithDeprecated(BaseModel):
13 deprecated_field: bool = Field(deprecated=True)
14
15class WithNoAllowEmpty(BaseModel):
16 can_be_empty: bool = Field(allowEmptyValue=False)
17
18print(openapi_params(WithDescription)[0]["description"])
19print(openapi_params(InPath)[0]["in"])
20print(openapi_params(WithDeprecated)[0]["deprecated"])
21print(openapi_params(WithNoAllowEmpty)[0]["allowEmptyValue"])

and we will see the response:

1>>> 'Hello World Use Me!'
2>>> 'path'
3>>> 'True'
4>>> 'False'

If we hardcoded the ParameterDict class, this can raise multiple errors:

  • ValueError – If any of the fields are complex types.
  • ValueError – If location is not a value supported by ParameterDict.in.
  • ValueError – If you try to set required to False on a field whose location is "path".
  • ValueError – If you try to set allowEmptyValue on a field whose location is not "query".

My-Brain-Hurts

I know most of what we see right now need to get deep somehow in OpenAPI, but we this is the simple way to build a ParameterDict class.

Django QueryDictModel

In an HttpRequest object, the GET and POST attributes are instances of django.http.QueryDict, a dictionary-like class customized to deal with multiple values for the same key. This is necessary because some HTML form elements, notably <select multiple>, pass multiple values for the same key.

The QueryDicts at request.POST and request.GET will be immutable when accessed in a normal request/response cycle. To get a mutable version you need to use QueryDict.copy().

This is how the official Django documentation explain it, that’s why pyngo come with idea of pyngo.querydict_to_dict() and pyngo.QueryDictModel are conveniences for building a pydantic.BaseModel from a django.QueryDict.

let’s see how it works, we will use the following model:

1from django.http import QueryDict
2
3class Model(QueryDictModel):
4 id: int
5 name: str
6
7Model.parse_obj(QueryDict("id=12&name=yezz"))

the result is:

1>>> Model(id=12, name='yezz')

Convert a Django.http.QueryDict to a dict under the constraints introduced on the types of fields of a pydantic.BaseModel.

Let’s took a look at the pyngo.querydict_to_dict() and pyngo.QueryDictModel function:

Note: Don’t forget to Setup the Django Project.

1from typing import List
2from django.http import QueryDict
3from pydantic import BaseModel
4from pyngo import QueryDictModel, querydict_to_dict
5
6class Model(BaseModel):
7 single_param: int
8 list_param: List[str]
9
10class QueryModel(QueryDictModel):
11 single_param: int
12 list_param: List[str]
13
14query_dict = QueryDict("single_param=20&list_param=Life")
15
16print(Model.parse_obj(querydict_to_dict(query_dict, Model)))
17print(QueryModel.parse_obj(query_dict))

as a result we will see:

1>>> Model(single_param=20, list_param=['Life'])
2>>> QueryModel(single_param=20, list_param=['Life'])

This is a very simple way to build a pydantic.BaseModel from a django.http.QueryDict.

ValidationError in Django REST Framework

Extracting the arguments from a pydantic.ValidationError and convert them to a dictionary whose format matches those used by the details for an error in Django Rest Framework, this work is done by creating this function pyngo.drf_error_details and calling it on the pydantic.ValidationError:

  • pyngo.drf_error_details() will propagate any errors from Pydantic:
1from pydantic import BaseModel, ValidationError
2from pyngo import drf_error_details
3
4class Model(BaseModel):
5 id: int
6 name: str
7
8data = {"id": "Cat"}
9
10try:
11 Model.parse_obj(data)
12except ValidationError as e:
13 print(drf_error_details(e))

As a result we will see:

1{
2 'name': ['field required'],
3 'id': ['value is not a valid integer']
4}
  • Let’s see the Errors descend into nested fields:
1from typing import List
2from pydantic import BaseModel, ValidationError
3from pyngo import drf_error_details
4
5class Framework(BaseModel):
6 id: int
7
8class Language(BaseModel):
9 framework: List[Framework]
10
11data = {"Framework": [{"id": "not_a_number"}]}
12
13try:
14 Framework.parse_obj(data)
15except ValidationError as e:
16 print(drf_error_details(e))

We will see:

1{
2 'framework': {
3 '0': {
4 'id': ['value is not a valid integer']
5 },
6 '1': {
7 'id': ['field required']
8 }
9 }
10}

The_End

To Integrate Pydantic with Django or Django Rest Framework, try to see some articles or pre-built packages like Djantic or Django-Ninja a web framework for building APIs with Django and Python 3.6+ type hints inspired by FastAPI.

In this article we just take a deep look into my pre-built package thats could help you integrate pydantic with Django and Django Rest Framework Natively.

Resources:

More articles from Obytes

Apache Superset on AWS ECS

How to deploy a dockerized Superset infrastructure on AWS ECS using Terraform

December 9th, 2021 · 5 min read

GO Serverless! Part 4 - Realtime Interactive and Secure Applications with AWS Websocket API Gateway

Build Realtime, Interactive and Secure Applications with AWS Websocket API Gateway

November 17th, 2021 · 19 min read

ABOUT US

Our mission and ambition is to challenge the status quo, by doing things differently we nurture our love for craft and technology allowing us to create the unexpected.