Pydantic with Flask
Posted on
by Kevin FoongI haven't seen many articles on the internet describing how to use Pydantic together with Flask so I thought of providing an overview of what I did to get this to work. For those who don't know, Pydantic is a Python library used to parse and validate data. It is typically used to ensure that data sent to your API enpoints is in the correct format and type.
- First we install a handy library Flask-Pydantic. This library is used to integrate Pydantic with Flask.
pip install Flask-Pydantic
- Next we create Pydantic schema models. These models define the required fields for the endpoint. A model is just a class that inherits from Pydantic's
BaseModel
. I normally have all models in a separatemodels_schema.py
file. - Below is an excerpt from my
models_schema.py
file.
Some notes,- In this example,
CustomerPostModel
defines the fields the/users/customer
(POST) endpoint expects to receive. - We specify all the field's minimum and maximum length, whether they are optional or mandatory and their data type (such as integer).
- We can add custom validation to any field. in this example we are making sure the email field conforms to a certain email format.
- Most Flask applications that interact with a database will use SQLAlchemy as the ORM. I note that there are some provisions by Pydantic to enable you to integrate SQLAlchemy with your Pydantic model but I couldn't get it to work, at least not elegantly. Nevertheless I found separating the SQLAlchemy model and the Pydantic model separately was easy enough, with the caveat that sometimes you may need to double up on changes in two places. One scenario where you will need to do this is when you are changing the length of a field.
from typing import Optional import re from pydantic import BaseModel, validator, constr from .models import User class CustomerPostModel(BaseModel): # mandatory field email: constr(min_length=3, max_length=30) password: constr(min_length=3, max_length=20) first_name: constr(min_length=1, max_length=50) last_name: constr(min_length=1, max_length=50) # optional denotes the field is optional phone: Optional[constr(max_length=20)] address: Optional[constr(max_length=500)] # accepts integers only created_by_id: Optional[int] # custom validation on email field @validator('email') def email_valid(cls, v): email = v.lower() if re.match('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', email) is None: raise ValueError('email provided is not valid') if User.query.filter_by(email=email).first(): raise ValueError('email already registered') return email
- In this example,
- Now all we need is to add a
@validate
decorator specifying the model to use to the endpoint. Pydantic will now validate the data sent to this endpoint to make sure it conform's to the model's specification.
Also note that we specified the name of the model via thebody
parameter. This validates data sent in the body of the request, for example from a form. We could also have created a separate model to validate data sent via URL query paramaters. In this case we will use thequery
parameter instead.
from flask_pydantic import validate @app.route('users/customer', methods=['POST']) # add Pydantic schema model to this endpoint @validate(body=CustomerPostModel) def create_customer(): # do database stuff customer = Customer() customer.from_dict( data, update_by=token_auth.current_user().email, new_user=True) db.session.add(customer) db.session.commit() # return sucess response = jsonify(customer.to_dict()) response.status_code = 201 response.headers['Location'] = url_for('api_v1.get_user', id=customer.user.id) return response
- Now if we send some incorrect data to the endpoint, Pydantic will send back a 400 Bad Request error with description of the problem. All this will happen automatically and the rest of the Flask code will not be executed. Example error response below on 2 fields.
{ "validation_error": { "body_params": [ { "ctx": { "limit_value": 3 }, "loc": [ "password" ], "msg": "ensure this value has at least 3 characters", "type": "value_error.any_str.min_length" }, { "loc": [ "created_by_id" ], "msg": "id does not belong to a valid user", "type": "value_error" } ] } }
This brief overview has shown you how easy it is to integrate Pydantic and Flask together. I hope you will give Pydantic a try in your next API project!