Schema Validation with Pydantic

Why do we need schema?

  • It's a pain to get all the values from the body

  • The client can send whatever data they want

  • The data isn't getting validated

  • We ultimately want to force the client to send data in a schema that we expect

Pydantic

  • This is already installed if you have used the [all] installation flag

pip install fastapi[all]
  • It is it's own separate module and has nothing to do with FastAPI (can be used separately)

  • FastAPI uses it so that it can define a schema

So how do you validate the data using Pydantic?

  • First you import the BaseModel from pydantic

from pydantic import BaseModel
  • Then you define a class that inherits from the BaseModel and set up the schema

Note: In our case we just want the Title and Content to create a post

Logically these should be stings to we will set them as such

class Post(BaseModel):
    title: str
    content: str
  • Once you have defined the class correctly, you can pass this to the function for the Path Operation

@app.post("/createpost")
def create_posts(post: Post):
    print(post)
    return {"data": "new post"}
  • We are referencing the Post class and assigning it to a variable on line 2

  • After which we are printing the post to the console

  • The good thing about doing things in this way is that our data gets validated automatically and our API does not accept integers for example

  • It only accepts strings as input, otherwise it throws an error

If you check the console you can see the following output with the data validated:

INFO:     Waiting for application startup.
INFO:     Application startup complete.
title='top gun' content='check out my top gun'
INFO:     127.0.0.1:48886 - "POST /createpost HTTP/1.1" 200 OK

Extracting the data:

  • This is made easy by defining the schema as mentioned above

  • All you have to do is add the field you want it to print

@app.post("/createpost")
def create_posts(post: Post):
    print(post.title) # Note that we added the title here
    return {"data": "new post"}

Output:

INFO:     Application startup complete.
top gun
INFO:     127.0.0.1:33096 - "POST /createpost HTTP/1.1" 200 OK

Data validation:

What happens if we remove the title field?

  • If we remove the title from the HTTP Packet in postman and have it as such:

{
    "content": "check out my top gun"
}
  • Once you send the packet you will get the following error message:

{
    "detail": [
        {
            "loc": [
                "body",
                "title"
            ],
            "msg": "field required",
            "type": "value_error.missing"
        }
    ]
}
  • It is throwing an error as it is expecting both a title and content to process

  • It let's us know that the value is missing: "type": "value_error.missing"

What happens if we provide a string as input for title?

  • We are sending the HTTP Packet via Postman as such:

{
    "title": 1, 
    "content": "check out my top gun"
}
  • It will not throw an error as it will try to convert whatever data we are trying to give it to the type specified (string)

INFO:     Application startup complete.
title='1' content='check out my top gun'
INFO:     127.0.0.1:56232 - "POST /createpost HTTP/1.1" 200 OK

Letting the user define data

  • We will have to extend our class with to look as follows:

class Post(BaseModel):
    title: str
    content: str
    published: bool = True 
    # The above line decides if a Post gets published
    # Also defaults to True
  • Now you have 3 options on sending the HTTP Packet via Postman

// Setting it to True
{
    "title": "top gun", 
    "content": "check out my top gun",
    "published": true
}

// Setting it to False
{
    "title": "top gun", 
    "content": "check out my top gun",
    "published": false
}

// Not specifying it as it defaults to True
{
    "title": "top gun", 
    "content": "check out my top gun"
}

Setting an optional field that defaults to None

  • You will need to import Optional from the typing library

from typing import Optional
  • Then we will extend the class for post ratings

class Post(BaseModel):
    title: str
    content: str
    published: bool = True
    rating: Optional[int] = None
    # Note that the above line is Optional
    # Requires an integer value (makes sense for ratings)
    # Defaults to None
  • Now we can send HTTP Packets like so:

// Not Specifed as it is optional, returns None
{
    "title": "top gun", 
    "content": "check out my top gun"
}

// Specified with number, returns 4
{
    "title": "top gun", 
    "content": "check out my top gun"
    "rating": 4
}

// Specifing but with a string, returns error - see below
{
    "title": "top gun", 
    "content": "check out my top gun"
    "rating": "hello"
}

// Error Recived:
{
    "detail": [
        {
            "loc": [
                "body",
                "rating"
            ],
            "msg": "value is not a valid integer",
            "type": "type_error.integer"
        }
    ]
}
  • The error raised is that the value provided is not integer: "type": "type_error.integer"

Note that all these values that you provide via HTTP Packet are stored in a pydantic model - which is a specific type of storage

  • All these models have a .dict method to convert the data to a dictionary

  • So if you ever need to convert the data, use as follows:

@app.post("/createpost")
def create_posts(post: Post):
    post.dict() # This will convert to dictionary data structure

Last updated