Post

Response Model

Let's learn Response Models for Clean and Secure APIs

Response Model

When building APIs, it’s crucial to have full control over what your endpoints return - not just for security, but for clarity and maintainability. FastAPI’s response_model parameter helps you do exactly that. It defines the shape of the output using Pydantic models, allowing FastAPI to automatically validate and serialize responses.

We can take advantage of these characteristics in useful ways:

  1. Unnecessary fields can be excluded from the response data.
  2. The response structure can be clearly presented in the auto-generated documentation.
  3. Data type validation helps prevent bugs.

Basic Usage

In the example, we defined the following Pydantic models. In the response, we can see that the password field which was not explicitly defined is excluded. Meanwhile, fields like description, tax, and tags appear in the response with their default values, even though they weren’t included in the return statement.

Using get

1
2
3
4
5
6
7
8
9
10
class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []

@router.get("/response_model/case1", response_model=Item)
async def read_items():
    return {"name": "Portal Gun", "price": 42.0, "password": "1234"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
try:
    url = "http://127.0.0.1:7249/ds2man/basic/response_model/case1"

    async with aiohttp.ClientSession(
        trust_env=True, timeout=aiohttp.ClientTimeout(total=120)
    ) as session:
        async with session.get(url, params=data) as response: 
            response.raise_for_status()
            await log_response_info(response)

except ClientResponseError as e:
    print(f"[ClientResponseError] {e.status}: {e.message}")
except Exception as e:
    print(f"[Unhandled Exception] {e}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Response Info]
Status       : 200 OK
URL          : http://127.0.0.1:7249/ds2man/basic/response_model/case1
Method       : GET
Content-Type : application/json
Charset      : None
Cookies      : {}
Headers:
{'Content-Length': '74',
 'Content-Type': 'application/json',
 'Date': 'Sun, 04 May 2025 01:47:07 GMT',
 'Server': 'uvicorn'}
OK?          : True

JSON Body:
type(data): <class 'dict'>
{'description': None,
 'name': 'Portal Gun',
 'price': 42.0,
 'tags': [],
 'tax': None
 }

Both List and Dict types are supported.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []
    
@router.get("/response_model/case2", response_model=List[Item])
async def read_items():
    return [
        {"name": "Portal Gun", "price": 42.0},
        {"name": "Plumbus", "price": 32.0},
    ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
try:
    url = "http://127.0.0.1:7249/ds2man/basic/response_model/case2"

    async with aiohttp.ClientSession(
        trust_env=True, timeout=aiohttp.ClientTimeout(total=120)
    ) as session:
        async with session.get(url, params=data) as response:
            response.raise_for_status()
            await log_response_info(response)

except ClientResponseError as e:
    print(f"[ClientResponseError] {e.status}: {e.message}")
except Exception as e:
    print(f"[Unhandled Exception] {e}")
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
[Response Info]
Status       : 200 OK
URL          : http://127.0.0.1:7249/ds2man/basic/response_model/case2
Method       : GET
Content-Type : application/json
Charset      : None
Cookies      : {}
Headers:
{'Content-Length': '148',
 'Content-Type': 'application/json',
 'Date': 'Sun, 04 May 2025 02:08:47 GMT',
 'Server': 'uvicorn'}
OK?          : True

JSON Body:
type(data): <class 'list'>
[{'description': None,
  'name': 'Portal Gun',
  'price': 42.0,
  'tags': [],
  'tax': None},
 {'description': None,
  'name': 'Plumbus',
  'price': 32.0,
  'tags': [],
  'tax': None}
  ]

Using post

1
2
3
4
5
6
7
8
9
10
class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []

@router.post("/response_model/case3", response_model=Item)
async def create_item(item: Item):
    return item
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try:
    url = "http://127.0.0.1:7249/ds2man/basic/response_model/case3"
    data = {
        "description": "Hello",
        "name": "DS2Man",
        "price": 100.1,
        "tags": ["FastAPI", "Response Model"]
    }

    async with aiohttp.ClientSession(
        trust_env=True, timeout=aiohttp.ClientTimeout(total=120)
    ) as session:
        async with session.post(url, json=data) as response:
            response.raise_for_status()
            await log_response_info(response)

except ClientResponseError as e:
    print(f"[ClientResponseError] {e.status}: {e.message}")
except Exception as e:
    print(f"[Unhandled Exception] {e}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Response Info]
Status       : 200 OK
URL          : http://127.0.0.1:7249/ds2man/basic/response_model/case3
Method       : POST
Content-Type : application/json
Charset      : None
Cookies      : {}
Headers:
{'Content-Length': '100',
 'Content-Type': 'application/json',
 'Date': 'Sun, 04 May 2025 02:04:21 GMT',
 'Server': 'uvicorn'}
OK?          : True

JSON Body:
type(data): <class 'dict'>
{'description': 'Hello',
 'name': 'DS2Man',
 'price': 100.1,
 'tags': ['FastAPI', 'Response Model'],
 'tax': None
 }
This post is licensed under CC BY 4.0 by the author.