# 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 %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.arkannis.net/programming/python/frameworks/fastapi/schema-validation-with-pydantic.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
