Flask and Pydantic

Introduction

In this tutorial we are going to learn how to use Pydantic together with Flask to perform validation of query parameters and request bodies.

As we have already covered in the introductory tutorial about Pydantic, this library allows to define models that can be used for data deserialization and validation. A common use case where we receive external data that we cannot trust and that needs to be parsed to some model is when developing a web server.

As such, we are going to learn how to use the Flask-Pydantic library, which allows us to use Pydantic models to perform the validation of query parameters and request bodies on Flask routes.

Note that the usage of this library is not strictly necessary to combine Flask and Pydantic, since we can just access the request raw data and pass it to our Pydantic models. Nonetheless, this library offers a more elegant interface out of the box, as we will see below.

If you use pip, you can install this library with the following command:

pip install Flask-Pydantic

The code from this tutorial was tested with Python v3.7.2, on Windows. The library versions used were the following:

  • Flask: 2.1.2
  • Pydantic: 1.9.0
  • Flask-Pydantic: 0.9.0

Defining a Pydantic class for query parameters

We will start by the library imports. The first import will be the Flask class from the flask module, so we can create our application. After that we will import the BaseModel class from pydantic. This is the class that our pydantic models should extend.

from flask import Flask
from pydantic import BaseModel

We will also import the validate decorator from the flask_pydantic module. We will later use this decorator in our route.

from flask_pydantic import validate

Now that we have done all our imports, we will take care of defining our pydantic model that will be responsible to validate the query parameters for our route.

Although we are not going to implement any functionality in the route, let’s assume that our endpoint allows us to get a list of persons. Let’s also assume that we offer the possibility of filtering that list by two parameters:

  • first_name: a string containing a particular person first name that we want to search for
  • is_married: a Boolean that specifies if we want to bring married persons (true) or unmarried (false)

Naturally, we are assuming that these filters will be passed as query parameters. As such, we will define a class called QueryParams (which inherits from the BaseModel). This class will have the fields mentioned above, with the respective type hints (str and bool).

class QueryParams(BaseModel):
  first_name: str
  is_married: bool

Next we will create our Flask app.

app = Flask(__name__)

After this we will declare our route. Let’s assume that this endpoint will correspond to the path “/person” and that it will answer to HTTP GET requests. With Flask only, we declare the route as this:

@app.route("/person", methods=["GET"])
def get():
    #implementation, access query params

But, in our case, we want to validate that the query parameters that are passed to our request are valid per our pydantic model definition. One easy way could be accessing the request args parameter directly and then try to instantiate our QueryParams model. Nonetheless, Flask-Pydantic offers a more elegant way where we can apply the validate decorator to our route and declare a parameter called query in the route handling function. The type of this parameter must be our pydantic class QueryParams.

@app.route("/person", methods=["GET"])
@validate()
def get(query: QueryParams):
   #implementation

In our route implementation we will simply return this query object back as a response. As output, later when testing the code, we should see the serialized JSON corresponding to this query object.

@app.route("/person", methods=["GET"])
@validate()
def get(query: QueryParams):

  return query

To finalize, we will run our app with a call to the run method on our app object. The complete code is available below

from flask import Flask
from pydantic import BaseModel
from flask_pydantic import validate

class QueryParams(BaseModel):
  first_name: str
  is_married: bool


app = Flask(__name__)

@app.route("/person", methods=["GET"])
@validate()
def get(query: QueryParams):

  return query

app.run(host='0.0.0.0', port=8080)

To test the code, simply open a web browser of your choice and type the following in the address bar:

http://127.0.0.1:8080/person?first_name=john&is_married=false

Note that we are passing the two query parameters that we have defined in our QueryParams class, so the request should be valid. As output we should get a result similar to figure 1. As can be seen, we have obtained the JSON representation of our QueryParams object.

Output of the route, with the result of the Pydantic model.
Figure 1 – Output of the route.

Next we will remove the query parameters:

http://127.0.0.1:8080/person

If we do so, we should get a result similar to figure 2. As can be seen, we get two errors indicating that both “first_name” and “is_married” are required fields.

Errors on the route due to wrong type in one of the query parameters.
Figure 2 – Errors on the route due to the query parameters not having been passed.

We will also try to add the query parameters back, but setting an invalid type (a string) to the Boolean parameter:

http://127.0.0.1:8080/person?first_name=john&is_married=doe

As output we will get a result like the one shown in figure 3. In this case we get a type error, since the value we provided cannot be parsed to a Boolean.

Errors on the route due to wrong type in one of the query parameters.
Figure 3 – Errors on the route due to wrong type in one of the query parameters.

Making query param fields optional

In the previous example we made a very strong assumption that both query parameters were required. As we could see in our tests, if we didn’t pass one of the fields (first_name and is_married), we would get an error. Nonetheless, it is very common that query parameters in an endpoint are optional, specially in an example such as the one we have built, where they represent some filtering criteria.

As such, in this section, we will do a slight change to our code to make the fields optional. To start, we will import the Optional hint, additionally to all the imports we already had.

from typing import Optional

Then, we will add this type hint to both parameters of our model. The complete model is shown below.

class QueryParams(BaseModel):
  first_name: Optional[str]
  is_married: Optional[bool]

The rest of the code will stay the same. For completion, the full code can be seen below.

from flask import Flask
from pydantic import BaseModel
from flask_pydantic import validate
from typing import Optional


class QueryParams(BaseModel):
  first_name: Optional[str]
  is_married: Optional[bool]


app = Flask(__name__)

@app.route("/person", methods=["GET"])
@validate()
def get(query: QueryParams):

  return query

app.run(host='0.0.0.0', port=8080)

This time, if we access the endpoint from a web browser without setting any query parameters, we will no longer get an error.

http://127.0.0.1:8080/person

Figure 4 shows what happens in this case. As can be seen, we didn’t get an error and the object representing the query parameters had both fields set to null.

Accessing the /person endpoint without specifying query params.
Figure 4 – Accessing the /person endpoint without specifying query params.

Naturally we can pass one of the parameters and omit the other:

http://127.0.0.1:8080/person?is_married=true

Figure 5 exemplifies this use case.

 Setting only one query parameter when reaching the /person endpoint.
Figure 5 – Setting only one query parameter when reaching the /person endpoint.

Note that type validation is still performed if we pass the query parameter. For example, if we set the is_married field to some value that is not a Boolean, we will still get an error:

http://127.0.0.1:8080/person?is_married=something

Figure 6 illustrates the mentioned error.

Validation error on the Boolean query parameter.
Figure 6 – Validation error on the Boolean query parameter.

Defining a Pydantic class for the request Body

In this section we are going to cover how to define a class to desserialize and validate the request body. The approach will be very similar to what we have covered in the first code section for the query parameters. Our endpoint will answer to POST requests and receive a body payload representing a Person entity.

Our imports will be the same we have seen before.

from flask import Flask
from pydantic import BaseModel
from flask_pydantic import validate

Then we will define the pydantic model that corresponds to the expected body. Like already mentioned, we will assume, for exemplification purposes, that we will receive a payload representing a Person entity. It will have the following fields:

  • first_name and last_name, which are both strings
  • age, which is an integer
  • is_married, which is a Boolean

The class is shown below.

class Body(BaseModel):
  first_name: str
  last_name: str
  age: int
  is_married: bool

Next we will take care of creating our Flask app.

app = Flask(__name__)

After this we will declare our route. Once again, we assume that this endpoint will correspond to the path “/person” but, this time, it will answer to HTTP POST requests. Once again, besides the route decorator, we will add the validate decorator. Additionally, the route handling function will receive a parameter called body. The type of this parameter must be our pydantic Body class.

@app.route("/person", methods=["POST"])
@validate()
def create(body: Body):
    # Route implementation

Once again, in the route implementation, we will simply return the parsed Body object as output of the route handling function. The full route definition is shown below.

@app.route("/person", methods=["POST"])
@validate()
def create(body: Body):

  return body

The complete code is shown below and it already includes the call to the run method on the app object, so our Flask server starts listening to incoming requests.

from flask import Flask
from pydantic import BaseModel
from flask_pydantic import validate


class Body(BaseModel):
  first_name: str
  last_name: str
  age: int
  is_married: bool


app = Flask(__name__)

@app.route("/person", methods=["POST"])
@validate()
def create(body: Body):

  return body

app.run(host='0.0.0.0', port=8080)

To test the code, we can use a tool such as Postman to perform the HTTP POST request. Figure 7 below shows an example where we have passed a valid JSON payload as body of our request. Consequently, we got back the same object, as expected.

Using postman to perform a successful HTTP POST request to the /person endpoint.
Figure 7 – Using postman to perform a successful HTTP POST request to the /person endpoint.

If we omit all the fields from the body and send only an empty object, we will get 4 errors as output of our request, which correspond to the 4 missing fields defined in our Body model. This scenario is shown below in figure 8.

Validation errors on the request body.
Figure 8 – Validation errors on the request body.

Leave a Reply