Best Practice of handling FastAPI Schema
Tags: fastapi, schema, bestpractice, python
Published: 2022-02-19 09:37:49

---
title: Best Practice of handling FastAPI Schema
published: true
description: Best Practice of handling FastAPI Schema
tags: FastAPI, Schema, BestPractice, Python
//cover_image: https://direct_url_to_image.jpg
---

#### The Best Practice of handling FastAPI Schema

FastAPI Has a concept of Schema. DTO is more familiar name If you have developed at Spring or NestJS. Simply, DTO is Data Transfer Object and It is a kind of promise to exchange object information between methods or classes in a specific model.



FastAPI's schema depends on pydantic model.

```python
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    model: str
    manufacturer: str
    price: float
    tax: float
```

As you can see, schema is made by inheriting the pydantic Base Model. Now let's use this to construct an api endpoint.

```python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    model: str
    manufacturer: str
    price: float
    tax: float


@app.get("/")
async def root(
        item: Item
):
    return {"message": "Hello World"}

```

If you declare that you're gonna get an item schema at the basic endpoint,

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lf6ru5fy8b0d029y2e2i.png)

In this way, an example of receiving the model is automatically generated. You can use this in other ways like

```python
from fastapi import FastAPI, Depends
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    model: str
    manufacturer: str
    price: float
    tax: float


@app.get("/")
async def root(
        item: Item = Depends()
):
    return {"message": "Hello World"}

```

If you register the value "Depends" at schema,

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hgrty7akngb1ttiyvx4f.png)

You can see that the part that you want to receive as a Body has changed to Parameter. Doesn't it feel like You've seen it somewhere? Right. It is a common method for constructing search query. However, as can be seen above, it can be seen that there is a limit of required values. It's a search query, but it doesn't make sense that all members have a required option. So it is necessary to change all of this to optional.



very easily

```python
from pydantic import BaseModel
from typing import Optional

class Item(BaseModel):
    name: Optional[str]
    model: Optional[str]
    manufacturer: Optional[str]
    price: Optional[float]
    tax: Optional[float]

```

It can be made in this way, but if the default value of the schema members are optional, there is a hassle of removing all the Optional when configuring the PUT method endpoint later. Oh, it is recommended that anyone who has no clear distinction between the PUT method and the PATCH method here refer to [this article](https://www.baeldung.com/http-put-patch-difference-spring). In short, PUT is the promised method of changing the total value of an element, and PATCH is the method of changing some.



So I use this way.

```python
from pydantic.main import ModelMetaclass

class AllOptional(ModelMetaclass):
    def __new__(self, name, bases, namespaces, **kwargs):
        annotations = namespaces.get('__annotations__', {})
        for base in bases:
            annotations.update(base.__annotations__)
        for field in annotations:
            if not field.startswith('__'):
                annotations[field] = Optional[annotations[field]]
        namespaces['__annotations__'] = annotations
        return super().__new__(self, name, bases, namespaces, **kwargs)

```

This class has the function of changing all member variables of the class to Optional when declared as metaclass.

```python
from fastapi import FastAPI, Depends
from pydantic import BaseModel
from pydantic.main import ModelMetaclass
from typing import Optional

app = FastAPI()


class AllOptional(ModelMetaclass):
    def __new__(self, name, bases, namespaces, **kwargs):
        annotations = namespaces.get('__annotations__', {})
        for base in bases:
            annotations.update(base.__annotations__)
        for field in annotations:
            if not field.startswith('__'):
                annotations[field] = Optional[annotations[field]]
        namespaces['__annotations__'] = annotations
        return super().__new__(self, name, bases, namespaces, **kwargs)


class Item(BaseModel):
    name: str
    model: str
    manufacturer: str
    price: float
    tax: float


class OptionalItem(Item, metaclass=AllOptional):
    pass


@app.get("/")
async def root(
        item: OptionalItem = Depends()
):
    return {"message": "Hello World"}

```

If you inherit the class made of the pydantic Base Model and declare 'metaclass=AllOptional' there,

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nf3q98fxzlrk2y4m08qp.png)

You can see Parameters without required option.





However, when dealing with a schema in practice, there are things that are not needed when putting it in and necessary when bringing it later. It is common for id, created_datetime, updated_datetime, etc. And sometimes you have to remove some members and show them. In spring or nest, you can flexibly cope with this situation by using the setFieldToIgnore function or the Omit class, but as anyone who has dealt with FastAPI can see, there are no such things.



So after a lot of trial and error, i established a way of using FastAPI schema like the DTO we used before. It would be irrelevant to say that this is the best practice now. Why? I looked up numerous examples, but there was no perfect alternative, so I defined them myself and made them.



This is an example using the schema called Item used above.

```python
from pydantic import BaseModel

class BaseItem(BaseModel):
    name: str
    model: str
    manufacturer: str
    price: float
    tax: float
```

The most basic members are defined as BaseItem. It can be seen as defining the elements used in Upsert. The pydantic model will basically be a schema used in the PUT method because it will be treated as Required if it is not set as Optional or = None.

```python
class Item(BaseItem, metaclass=AllOptional):
    id: int
    created_datetime: datetime
    updated_datetime: datetime
```

It is an item that inherits BaseItem. Even if the class inherited the Base Model is inherited, the inherited class also becomes a pydantic model, so it can be declared this simply. It is used to throw items that are not used in Upsert, including basic elements. This is a schema mainly used to inquire items, so wouldn't it be okay not to attach Optional...? But if there is a non-Optional member and its value has no Value, an error is occurred.

```python
class FindBase(BaseModel):
    count: int
    page: int
    order: str


class FindItem(FindBase, BaseItem, metaclass=AllOptional):
    pass

```

Parameters used to find items can be declared in this way. It inherits the BaseItem and FindBase declared above and treats them both as options with metaclass=AllOptional to remove the required option of the parameter. And lastly,

```python
class Omit(ModelMetaclass):
    def __new__(self, name, bases, namespaces, **kwargs):
        omit_fields = getattr(namespaces.get("Config", {}), "omit_fields", {})
        fields = namespaces.get('__fields__', {})
        annotations = namespaces.get('__annotations__', {})
        for base in bases:
            fields.update(base.__fields__)
            annotations.update(base.__annotations__)
        merged_keys = fields.keys() & annotations.keys()
        [merged_keys.add(field) for field in fields]
        new_fields = {}
        new_annotations = {}
        for field in merged_keys:
            if not field.startswith('__') and field not in omit_fields:
                new_annotations[field] = annotations.get(field, fields[field].type_)
                new_fields[field] = fields[field]
        namespaces['__annotations__'] = new_annotations
        namespaces['__fields__'] = new_fields
        return super().__new__(self, name, bases, namespaces, **kwargs)

```

This is a metaclass that supports the Omit function. I needed it, but I couldn't find it, so I made it.

```python
class BaseItem(BaseModel):
    name: str
    model: str
    manufacturer: str
    price: float
    tax: float
    

class OmittedTaxPrice(BaseItem, metaclass=Omit):
    class Config:
        omit_fields = {'tax', 'price'}

```

If you declare omit_fields in class Config in this way, you can get a new schema with removed fields.



※ And one thing to note is that metaclass is executed every time a class is created, so only one class involved in an inheritance relationship can be declared. For example

```python
class BaseItem(BaseModel):
    name: str
    model: str
    manufacturer: str
    price: float
    tax: float


class Item(BaseItem, metaclass=AllOptional):
    id: int
    created_datetime: datetime
    updated_datetime: datetime



class TestItem(Item):
    additional_number: int

```

If the Item declared metaclass=AllOptional is inherited from TestItem, AllOptional is executed twice. Once when creating an Item class, once again when creating a TestItem that inherits the Item. Therefore, the additional_number, a member variable of TestItem that inherits the Item, is also Optional. That is metaclass. So, if you declare another metaclass like Omit, the program is broken.

```python
# ...
class TestItem(Item, metaclass=AllOptional)
    pass
# ...
# `TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases`
```

So, it is good to declare and use metaclass in the last inheritance class before it is used as schema. This makes easy to recognize its function.

Anyway, if you use the above-mentioned four http methods, GET, POST, PUT, and PATCH, which are often used, it consists of this. It can also be found in my git repo. https://github.com/jujumilk3/fastapi-best-practice/tree/schema

```python
from datetime import datetime
from fastapi import FastAPI, Depends
from pydantic import BaseModel
from pydantic.main import ModelMetaclass
from typing import Optional, List


app = FastAPI()


class AllOptional(ModelMetaclass):
    def __new__(self, name, bases, namespaces, **kwargs):
        annotations = namespaces.get('__annotations__', {})
        for base in bases:
            annotations.update(base.__annotations__)
        for field in annotations:
            if not field.startswith('__'):
                annotations[field] = Optional[annotations[field]]
        namespaces['__annotations__'] = annotations
        return super().__new__(self, name, bases, namespaces, **kwargs)


class Omit(ModelMetaclass):
    def __new__(self, name, bases, namespaces, **kwargs):
        omit_fields = getattr(namespaces.get("Config", {}), "omit_fields", {})
        fields = namespaces.get('__fields__', {})
        annotations = namespaces.get('__annotations__', {})
        for base in bases:
            fields.update(base.__fields__)
            annotations.update(base.__annotations__)
        merged_keys = fields.keys() & annotations.keys()
        [merged_keys.add(field) for field in fields]
        new_fields = {}
        new_annotations = {}
        for field in merged_keys:
            if not field.startswith('__') and field not in omit_fields:
                new_annotations[field] = annotations.get(field, fields[field].type_)
                new_fields[field] = fields[field]
        namespaces['__annotations__'] = new_annotations
        namespaces['__fields__'] = new_fields
        return super().__new__(self, name, bases, namespaces, **kwargs)


class BaseItem(BaseModel):
    name: str
    model: str
    manufacturer: str
    price: float
    tax: float


class UpsertItem(BaseItem, metaclass=AllOptional):
    pass


class Item(BaseItem, metaclass=AllOptional):
    id: int
    created_datetime: datetime
    updated_datetime: datetime


class OmittedTaxPrice(BaseItem, metaclass=Omit):
    class Config:
        omit_fields = {'tax', 'price'}


class FindBase(BaseModel):
    count: int
    page: int
    order: str


class FindItem(FindBase, BaseItem, metaclass=AllOptional):
    pass


@app.get("/")
async def root(
):
    return {"message": "Hello World"}


@app.get("/items/", response_model=List[Item])
async def find_items(
        find_query: FindItem = Depends()
):
    return {"hello": "world"}


@app.post("/items/", response_model=Item)
async def create_item(
        schema: UpsertItem
):
    return {"hello": "world"}


@app.patch("/items/{id}", response_model=Item)
async def update_item(
        id: int,
        schema: UpsertItem
):
    return {"hello": "world"}


@app.put("/items/{id}", response_model=Item)
async def put_item(
        id: int,
        schema: BaseItem
):
    return {"hello": "world"}


@app.get("/items/omitted", response_model=OmittedTaxPrice)
async def omitted_item(
        schema: OmittedTaxPrice
):
    return {"hello": "world"}

```

When configured in this way, the following swagger documents are generated.


**1. find items endpoint with optional parameters**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3lhgtyrbo7dbt8krum00.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hkwum5c6lwzsn9af9fe2.png)


**2. create item**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u55hpjpg3cvw3y7478ph.png)


**3. Put method**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/87bjbcjhqh8reuh6abhi.png)

If you omit the item name according to the usage of PUT METHOD, 422 error is returned to send it with some name. The PUT was formed by declaring that it would receive BaseItem as a body.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/no6ckhj8d3skm0bt8i76.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9xi0n86kspylhtwgquvh.png)


**4. PATCH method**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pryy1gijyjs6ixnkmtkt.png)



**5. Omitted**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i2h0zj5k1lu0p7u3ndgm.png)

In this way, we learned how to deal with FastAPIschema. If you use something, you may see more necessary functions, but the endpoint and docs configuration that you need this much is progressing without difficulty in my case. If you want to actively use FastAPI's schema to write docs, please refer to it to reduce trial and error.