# Request body parameters

When you need to send data from a client to your API, you send it as a request body. A request body is data sent by the client to your API. A response body is the data your API sends to the client.

Your API almost always has to send a response body. But clients don't necessarily need to send request bodies all the time. To declare a request body, you use Pydantic models (which can be compared with a class in Java).

When do you use a request body parameter?

To send data to your API, you should use a POST request with a request body parameter. Sending a body with a GET request has an undefined behavior in the specifications, nevertheless, it is supported by FastAPI, only for very complex/extreme use cases. As it is discouraged, the interactive docs with Swagger UI won't show the documentation for the body when using GET.

For the example that we will build up in this section, we would like to send a POST request with festival data representing Hellow Yellow (which was organised at our school at the beginning of the academic year).

Hellow Yellow

The information needed to create Hellow Yellow in the festival table of our database should be contained in a JSON object:

{
    "name": "Hellow Yellow",
    "location": "Geel",
    "startDate": "2023-10-05",
    "endDate": "2023-10-05",
    "province": "Antwerp",
    "comment": "They have free food!"
}
1
2
3
4
5
6
7
8

# Creating models.py

Somewhere in our program, we have to define the structure we expect when creating a new festival in our database. We will create Pydantic models to define our structure (also called a "model" or "schema") and will do this in a new file called models.py, which will be put in a new folder called models.

Project structure

As of now, our project structure will look like this:






 
 



📂myproject
├── 📄.env
├── 📄config.py
├── 📄database.py
├── 📄main.py
├── 📂models
|   └── 📄models.py
└── 📂queries
    └── 📄festival_queries.py
1
2
3
4
5
6
7
8
9

Below the code, you can find more detailed information about the contents of the models.py file:

from pydantic import BaseModel
from datetime import date

class Festival(BaseModel):
    name: str
    location: str
    startDate: date
    endDate: date
    province: str
    comment: str = None
1
2
3
4
5
6
7
8
9
10

At the top of our file, we will always import BaseModel from pydantic. BaseModel is some kind of super model and our own models will inherit properties from it. For more information about inheritance, we would like to refer to the course OO Programming.

The second import (importing date from the datetime library) is only necessary if you need to work with dates in your model.

from pydantic import BaseModel
from datetime import date
1
2

Next, we define our models. In this case, we only have one model, but you can have as many as necessary. You usually create a model for each table in your database (or at least the tables for which you would like to code POST requests). We define our Pydantic model using the keyword class, followed by the name of our model. To keep things clear, simply take the name of your table in SQL but use a capital letter (so festival becomes Festival). After the name of the model, put BaseModel between parenthesis.

class Festival(BaseModel):
1

Then we define the keys that we want to accept in our request body. In the case of our festival, we want to be able to receive the following information:

  • name
  • location
  • start date
  • end date
  • province
  • comment

It is important that the attributes of our model (which are the keys we want to accept in our request body) are indented one level so Python understands that they belong to the Pydantic model we're creating.

Make use of default Python data types to indicate which data type each attribute should be.

In case an attribute is not mandatory (with other words: the column would be nulls allowed or NA in our database table), we add = None behind it to give it the default value None in Python, which corresponds to null in our database. (Other default values than None are possible as well of course.)

    name: str
    location: str
    startDate: date
    endDate: date
    province: str
    comment: str = None
1
2
3
4
5
6

NA or NNA?

How do you know whether a value is mandatory (NNA) or not (NA)?

Have a look at the ERD of the database (if you have it somewhere), a good ERD contains this kind of information. The ERD of our simple festivals database looks like this:
Festivals data model

You can also have a look at the create script of your SQL database. For example of our festivals database:

CREATE TABLE IF NOT EXISTS festivaldb.festival (
	festivalId int PRIMARY KEY auto_increment,
    name varchar(100) NOT NULL,
    location varchar(100) NOT NULL,
    startDate date NOT NULL,
    endDate date NOT NULL,
    province varchar(100) NOT NULL,
    comment varchar(250)
);
1
2
3
4
5
6
7
8
9

As you can see, only the comment column is nulls allowed or NA in our table, all other columns are nulls not allowed or NNA, so mandatory.

What about the primary key?

You might wonder why the primary key (festivalId) is not used in our Pydantic model. The reason is that we do not want a user or external system to decide which value the primary key should get. Our database will manage this itself, the primary key is set to auto_increment in the creation script.

# Declare the request body as a parameter in the endpoint in main.py

Database interaction

In this section we will not have a database interaction yet. We will add this interaction in the section 'Database interaction' further down.

We will now have a look at the main.py file. You can find the code below. Endpoints of the previous chapter are left out here for simplicity. After the code, you can find an explanation.


 



 
 
 

from fastapi import FastAPI
from models import models

app = FastAPI()

@app.post("/festival")
def create_festival(festival: models.Festival):
    return festival
1
2
3
4
5
6
7
8

Line 2 in the code above imports the models.py file from the models folder so we can make use of our Pydantic models in main.py.

In lines 6 until 8, you can see a very basic structure for our endpoint. Please notice that our path operation decorator now contains post instead of get.

To use our Festivals Pydantic model, add it to your path operation function, declare it the same way you declared query parameters and declare its type as the model you created, which is Festival.

With just that Python type declaration, FastAPI will:

  • Read the body of the request as JSON.
  • Convert the corresponding types (if needed).
  • Validate the data.
    • If the data is invalid, it will return a nice and clear error, indicating exactly where and what was the incorrect data.
  • Give you the received data in the parameter festival.
    • As you declared it in the function to be of type Festival, you will also have editor support (e.g. completion) for all of the attributes and their types.
  • Generate JSON Schema definitions for your model, you can also use them anywhere else you like if it makes sense for your project.
  • Those schemas will be part of the generated OpenAPI schema, and used by the automatic documentation.

# Sending POST requests using Postman

Let's try out what we made so far using Postman. We will address our endpoint in the following way: http://127.0.0.1:8000/festival (opens new window)

There are some important things to note now:

  • The request type should be set to POST (at the left side of the URL)
  • In the available tabs, you should click on Body to enter the request body
  • Make sure raw and JSON are selected and type the request body in the pane that appears

Here's the JSON again that we mentioned earlier. You can copy and paste it to use as your request body.

{
    "name": "Hellow Yellow",
    "location": "Geel",
    "startDate": "2023-10-05",
    "endDate": "2023-10-05",
    "province": "Antwerp",
    "comment": "They have free food!"
}
1
2
3
4
5
6
7
8

Your request in Postman should now look like this:

POST request in Postman

WARNING

Depending on the width of Postman on your screen, the settings in the Body pane might look different: POST request in Postman

# Database interaction

We of course don't want to just return the information that was sent to the API as request body of a POST request. We usually want to store it in our database. In this section, you can find how it's possible to insert information in a table of your database.

# database.py

The database.py file already contains the necessary code to handle the logic we need to insert information in our database table. The lines of code that are responsible for adding the necessary data in your SQL table are highlighted below.


















 


 
 
 
 














import mysql.connector
import config

def connect_to_database():
    try:
        connection = mysql.connector.connect(host=config.db_hostname, user=config.db_username, password=config.db_password)
        return connection
    except mysql.connector.Error as error:
        print("Error connecting to database:", error)
        return error

def execute_sql_query(sql_query, query_parameters = None):
    connection = connect_to_database()
    result=''
    try:
        cursor = connection.cursor()
        cursor.execute(sql_query, query_parameters)
        if sql_query.upper().startswith("SELECT"):
            # executed for GET requests
            result = cursor.fetchall()
        else:
            # executed for POST requests
            connection.commit()
            result = True

        cursor.close()

    except mysql.connector.Error as exception:
        print("Error executing SQL query:", exception)
        result = exception


    finally:
        if connection.is_connected():
            connection.close()

        return result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

Committing changes

Committing in SQL refers to the process of permanently applying the changes made within a transaction to the database. When a transaction is committed, the changes become permanent and visible to other users or processes accessing the database. It essentially confirms that the transaction's operations have been successfully completed and updates the database accordingly.

# festival_queries.py

In festival_queries.py, we need to add an SQL query that can add the desired information to our table. An INSERT statement is what we need. The SQL query used to insert a festival into the festival table is highlighted below. Note that all the values that should be inserted are currently the placeholder %s. In main.py, we will provide the necessary values.









 

festival_name_query = "SELECT name, province FROM festivaldb.festival;"

festival_name_and_province_query = "SELECT name, province FROM festivaldb.festival;"

festivals_by_province_query = "SELECT name, province FROM festivaldb.festival WHERE province = %s;"

festivals_by_name_and_month_query = "SELECT name, location, startDate, endDate, province FROM festivaldb.festival WHERE name LIKE %s AND (date_format(startDate, '%m') = %s OR date_format(endDate, '%m') = %s);"

insert_festival_query = "INSERT INTO festivaldb.festival (name, location, startDate, endDate, province, comment) VALUES (%s, %s, %s, %s, %s, %s);"
1
2
3
4
5
6
7
8
9

# main.py

In main.py, we now add the lines that are highlighted below. They will retrieve the query from festival_queries.py and then execute the query while inserting the values of our festival in the same order as we declared them in our SQL query. Only if the execution of the query returns True, the festival object will be returned to indicate that our insertion succeeded.




 





 
 
 
 
 
 
 
 
 
 
 

from fastapi import FastAPI
import database
from models import models
from queries import festival_queries as queries

app = FastAPI()

@app.post("/festival")
def create_festival(festival: models.Festival):
    query = queries.insert_festival_query
    success = database.execute_sql_query(query, (
        festival.name,
        festival.location,
        festival.startDate,
        festival.endDate,
        festival.province,
        festival.comment,
    ))
    if success:
        return festival
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Try it out!

We should now be able to add a festival to our database using a POST request.

POST request with database interaction in Postman

Have a look in MySQL Workbench and verify that the festival was added in the festival table.

Table contents after the POST request

Primary key

Note that the primary key festivalId was filled in automatically. This is because of the auto_increment that was set for this primary key in the SQL creation script for this table.

Extra: manipulating the model

Inside of the path operation function, you can access all the attributes of the model object directly and apply your own logic:








 
 


from fastapi import FastAPI
from models import models

app = FastAPI()

@app.post("/festival")
def create_festival(festival: models.Festival):
    festival_dictionary = festival.dict()
    festival_dictionary["cityAndProvince"] = festival.location + ' (' + festival.province + ')'
    return festival_dictionary
1
2
3
4
5
6
7
8
9
10

In this example, we converted our festival object into a dictionary first. Next, we created a new key called cityAndProvince and gave it a value using the existing location and province of the festival object. Note that working with objects of Pydantic classes is very easy, the notation to get an attribute's value out of an object is object.attribute.

If we would try this out in Postman, the result would be as follows:

POST request in Postman with own logic

Last Updated: 3/12/2024, 10:29:25 AM