Ir para o conteúdo

Modelos Adicionais

Continuando com o exemplo anterior, será comum ter mais de um modelo relacionado.

Isso é especialmente o caso para modelos de usuários, porque:

  • O modelo de entrada precisa ser capaz de ter uma senha.
  • O modelo de saída não deve ter uma senha.
  • O modelo de banco de dados provavelmente precisaria ter uma senha criptografada.

Danger

Nunca armazene senhas em texto simples dos usuários. Sempre armazene uma "hash segura" que você pode verificar depois.

Se não souber, você aprenderá o que é uma "senha hash" nos capítulos de segurança.

Múltiplos modelos

Aqui está uma ideia geral de como os modelos poderiam parecer com seus campos de senha e os lugares onde são usados:

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: Union[str, None] = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: str | None = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

Sobre **user_in.dict()

O .dict() do Pydantic

user_in é um modelo Pydantic da classe UserIn.

Os modelos Pydantic possuem um método .dict() que retorna um dict com os dados do modelo.

Então, se criarmos um objeto Pydantic user_in como:

user_in = UserIn(username="john", password="secret", email="john.doe@example.com")

e depois chamarmos:

user_dict = user_in.dict()

agora temos um dict com os dados na variável user_dict (é um dict em vez de um objeto de modelo Pydantic).

E se chamarmos:

print(user_dict)

teríamos um dict Python com:

{
    'username': 'john',
    'password': 'secret',
    'email': 'john.doe@example.com',
    'full_name': None,
}

Desembrulhando um dict

Se tomarmos um dict como user_dict e passarmos para uma função (ou classe) com **user_dict, o Python irá "desembrulhá-lo". Ele passará as chaves e valores do user_dict diretamente como argumentos chave-valor.

Então, continuando com o user_dict acima, escrevendo:

UserInDB(**user_dict)

Resultaria em algo equivalente a:

UserInDB(
    username="john",
    password="secret",
    email="john.doe@example.com",
    full_name=None,
)

Ou mais exatamente, usando user_dict diretamente, com qualquer conteúdo que ele possa ter no futuro:

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
)

Um modelo Pydantic a partir do conteúdo de outro

Como no exemplo acima, obtivemos o user_dict a partir do user_in.dict(), este código:

user_dict = user_in.dict()
UserInDB(**user_dict)

seria equivalente a:

UserInDB(**user_in.dict())

...porque user_in.dict() é um dict, e depois fazemos o Python "desembrulhá-lo" passando-o para UserInDB precedido por **.

Então, obtemos um modelo Pydantic a partir dos dados em outro modelo Pydantic.

Desembrulhando um dict e palavras-chave extras

E, então, adicionando o argumento de palavra-chave extra hashed_password=hashed_password, como em:

UserInDB(**user_in.dict(), hashed_password=hashed_password)

...acaba sendo como:

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    hashed_password = hashed_password,
)

Warning

As funções adicionais de suporte são apenas para demonstração de um fluxo possível dos dados, mas é claro que elas não fornecem segurança real.

Reduzir duplicação

Reduzir a duplicação de código é uma das ideias principais no FastAPI.

A duplicação de código aumenta as chances de bugs, problemas de segurança, problemas de desincronização de código (quando você atualiza em um lugar, mas não em outros), etc.

E esses modelos estão compartilhando muitos dos dados e duplicando nomes e tipos de atributos.

Nós poderíamos fazer melhor.

Podemos declarar um modelo UserBase que serve como base para nossos outros modelos. E então podemos fazer subclasses desse modelo que herdam seus atributos (declarações de tipo, validação, etc.).

Toda conversão de dados, validação, documentação, etc. ainda funcionará normalmente.

Dessa forma, podemos declarar apenas as diferenças entre os modelos (com password em texto claro, com hashed_password e sem senha):

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

Union ou anyOf

Você pode declarar uma resposta como o Union de dois tipos, o que significa que a resposta seria qualquer um dos dois.

Isso será definido no OpenAPI com anyOf.

Para fazer isso, use a dica de tipo padrão do Python typing.Union:

Note

Ao definir um Union, inclua o tipo mais específico primeiro, seguido pelo tipo menos específico. No exemplo abaixo, o tipo mais específico PlaneItem vem antes de CarItem em Union[PlaneItem, CarItem].

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BaseItem(BaseModel):
    description: str
    type: str


class CarItem(BaseItem):
    type: str = "car"


class PlaneItem(BaseItem):
    type: str = "plane"
    size: int


items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
    return items[item_id]
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BaseItem(BaseModel):
    description: str
    type: str


class CarItem(BaseItem):
    type: str = "car"


class PlaneItem(BaseItem):
    type: str = "plane"
    size: int


items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
    return items[item_id]

Union no Python 3.10

Neste exemplo, passamos Union[PlaneItem, CarItem] como o valor do argumento response_model.

Dado que estamos passando-o como um valor para um argumento em vez de colocá-lo em uma anotação de tipo, precisamos usar Union mesmo no Python 3.10.

Se estivesse em uma anotação de tipo, poderíamos ter usado a barra vertical, como:

some_variable: PlaneItem | CarItem

Mas se colocarmos isso em response_model=PlaneItem | CarItem teríamos um erro, pois o Python tentaria executar uma operação inválida entre PlaneItem e CarItem em vez de interpretar isso como uma anotação de tipo.

Lista de modelos

Da mesma forma, você pode declarar respostas de listas de objetos.

Para isso, use o padrão Python typing.List (ou simplesmente list no Python 3.9 e superior):

from typing import List

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items/", response_model=List[Item])
async def read_items():
    return items
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items/", response_model=list[Item])
async def read_items():
    return items

Resposta com dict arbitrário

Você também pode declarar uma resposta usando um simples dict arbitrário, declarando apenas o tipo das chaves e valores, sem usar um modelo Pydantic.

Isso é útil se você não souber os nomes de campo / atributo válidos (que seriam necessários para um modelo Pydantic) antecipadamente.

Neste caso, você pode usar typing.Dict (ou simplesmente dict no Python 3.9 e superior):

from typing import Dict

from fastapi import FastAPI

app = FastAPI()


@app.get("/keyword-weights/", response_model=Dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}
from fastapi import FastAPI

app = FastAPI()


@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}

Em resumo

Use vários modelos Pydantic e herde livremente para cada caso.

Não é necessário ter um único modelo de dados por entidade se essa entidade precisar ter diferentes "estados". No caso da "entidade" de usuário com um estado que inclui password, password_hash e sem senha.