# 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

```python
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 <mark style="color:green;">`BaseModel`</mark> from <mark style="color:purple;">`pydantic`</mark>

```python
from pydantic import BaseModel
```

* Then you define a class that inherits from the <mark style="color:green;">`BaseModel`</mark> and set up the schema

{% hint style="info" %}
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
{% endhint %}

```python
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

```python
@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:

```bash
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

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

Output:

```bash
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:

```json
{
    "content": "check out my top gun"
}
```

* Once you send the packet you will get the following error message:

```json
{
    "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: <mark style="color:orange;">`"type": "value_error.missing"`</mark>

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

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

```json
{
    "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)

```bash
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:

```python
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

```json
// 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 <mark style="color:green;">`Optional`</mark> from the <mark style="color:purple;">`typing`</mark> library

```python
from typing import Optional
```

* Then we will extend the class for post ratings

```python
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:

```json
// 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: <mark style="color:orange;">`"type": "type_error.integer"`</mark>

{% hint style="warning" %}
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 <mark style="color:orange;">`.dict`</mark> method to convert the data to a dictionary
* So if you ever need to convert the data, use as follows:

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

{% endhint %}
