Response Model

2/25
Recursos

Aportes 12

Preguntas 1

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesión.

Lo que se hizo en esta clase tambien se hubiera podido hacer de otra manera valiendonos del parametro ‘response_model_exclude’ el cual recibe un set de strings con los atributos que deseamos excluir del response:

@app.post(
'/person/new', 
response_model=Person,
response_model_exclude={'password'}
) 

como ves usé el mismo modelo Person pero le especifiqué a FastAPI que no tuviera en cuenta el atributo password y asi no tuve que crear otro modelo de 0

PD: No estoy criticando lo que el profesor hizo ni diciendo que este mal hecho, solo comparto un approach diferente para que así conozcamos nuevos parametros que quizas nos sean utiles algun dia

Una buena práctica es generar un modelo Base con atributos generales e ir heredando de este para ir agregando que atributos extra necesitamos, por ejemplo BaseUser no contiene el password, y CreationUser que hereda de BaseUser solo agregaría el password dentro de ella, así se organiza un poco mejor los modelos.

Por si depronto no se acuerdan de activar el entorno virtual y carga la aplicación en el localhost

source venv/bin/activate
uvicorn main:app --reload

esta sentencias son en terminal dentro de la carpeta del proyecto.
Luego en el navegador colocamos la url de localhost

Para realiza una aplicación segura debemos tener dos temas en cuenta a la hora de la creación de la contraseña

  • La contraseña no se le envía al cliente
  • La contraseña no se almacena en texto plano

Response Model:
Es un atributo de nuestro path operation el cual es llamado desde nuestro Path Operation Decorator, el cual es utilizado para evitar el filtrado de información sensible

Leyendo la documentacion de FastAPI me di cuenta de un parametro que podemos añadir que hace algo bastante interesante, se trata de ‘response_model_exclude_unset’. Este parametro lo que hace es basicamente que si tu modelo de Pydantic tiene campos con default values estos sean excluidos si el usuario no los seteo (esto cuando tenemos el response_model_exclude_unset=True), si lo tienes en False entonces no va a hacer nada, mas info Aquí

Podrás seguir todos mis apuntes escritos en Notion en:
https://rain-scabiosa-74f.notion.site/Curso-de-FastAPI-Avanzado-189668eeee1a45168e46c5be56312967

Si te gusta dejame un corazoncito ♥

Response Model

Es un atributo de nuestro path operation el cual es llamado desde nuestro Path Operation Decorator, el cual es utilizado para evitar el filtrado de información sensible, en este definiremos un nuevo modelo que será el que se usará para el Response, por ejemplo si una persona realiza un POST con su contraseña en él, no podemos enviar la contraseña de regreso en el Response, esto sería un problema de seguridad bastante fuerte, entonces teniendo el modelo Person

Modelo Person Original

# Python
from typing import Optional

# Pydantic
from pydantic import BaseModel
from pydantic import Field, EmailStr

class Person(BaseModel):
    first_name: str = Field(
        ...,
        min_length=1,
        max_length=50,
        example='John',
    )
    last_name: str = Field(
        ...,
        min_length=1,
        max_length=50,
        example='Doe',
    )
    age: int = Field(
        ...,
        gt=0,
        le=110,
        example=25,
    )
    hair_color: Optional[HairColor] = Field(
        default=None,
        example=HairColor.blonde,
    )
    is_married: Optional[bool] = Field(
        default=None,
        example=False
    )
    email = EmailStr()

    password: str = Field(
        ...,
        min_length=8,
        max_length=50,
        example='password'
    )

Modelo PersonOut

Este es el modelo que se enviara por medio del Response

class PersonOut(BaseModel):
    first_name: str
    last_name: str
    age: int
    hair_color: Optional[HairColor]
    is_married: Optional[bool]
    email: EmailStr

Añadir el modelo en el Path Operation Decorator

@app.post('/person/new', response_model=PersonOut)
def create_person(person: Person = Body(...)):
    return person

Se añade el modelo modificado para el response dentro del Path Operation Decorator en el parametro Response Model

En Windows y en Linux:
- minimizar todo Ctrl + K y dps Ctrl + 0
- maxmizar todo Ctrl + K y dps Ctrl + J

El response model, es una utilidad de seguridad importante ya que evita que se filtre información que puede estar almacenada en nuestros modelos.
Su importancia radica en que al momento de que tu tengas X objeto (diccionario) que devuelvas, lo que hará FAST API será simplemente solo devolver los datos indicados en response_model.
Modelos y Enums a considerar

class HairColor(Enum):
    white = "white"
    red = "red"
    brown = "brown"
    blonde = "blonde"
    black = "black"

class Person(BaseModel):
    first_name: str = Field(...,
                            min_length=1,
                            max_length=50)
    last_name: str = Field(...,
                            min_length=1,
                            max_length=50)
    age: int = Field(
        ...,
        gt=0,
        lt=100
        )
    hair_color: Optional[HairColor] = Field(
        default=None
    )
    is_single: Optional[bool] = Field(
        default=True
    )
    email : Optional[EmailStr] = Field(default=None)
    class Config:
        schema_extra = {
            "example":{
                "first_name":"Rodrigo",
                "last_name": "Lopez",
                "age": 21,
                "hair_color":"black",
                "is_single":True
            }
        }
class Location(BaseModel):
    city: str
    state: str
    country: str
    class config:
        schema_extra ={
            "example": {
                "city":"Corregidora",
                "state":"Queretaro",
                "country":"Mexico"
            }
        }
class PersonOut(BaseModel):
    first_name: str = Field(...,
                            min_length=1,
                            max_length=50)
    last_name: str = Field(...,
                            min_length=1,
                            max_length=50)
    age: int = Field(
        ...,
        gt=0,
        lt=100
        )
    city: str
    state: str
    country: str

Consideremos la siguiente ruta de la API

@app.put("/person/{person_id}")
async def update_person(
    person_id: int = Path
    (..., gt=0, title="Here you put the id of the person",description="The id of the person must be greater than 0",example=777),
    person: Person = Body(...),
    location: Location = Body(...)):
    data = {**dict(person),**dict(location)}
    data["adicional"] = "jajajas"
    return data

Si nososotros no definimos un modelo tendremos la siguiente salida al momento de llamar la api

{
  "first_name": "Rodrigo",
  "last_name": "Lopez",
  "age": 21,
  "hair_color": "black",
  "is_single": true,
  "email": null,
  "city": "string",
  "state": "string",
  "country": "string",
  "adicional": "jajajas"
}

Ahora si modificamos la función especificamos el response_model, de la siguiente forma:

@app.put("/person/{person_id}", response_model=PersonOut)
async def update_person(
    person_id: int = Path
    (..., gt=0, title="Here you put the id of the person",description="The id of the person must be greater than 0",example=777),
    person: Person = Body(...),
    location: Location = Body(...)):
    data = {**dict(person),**dict(location)}
    data["adicional"] = "jajajas"
    return data

Entonces tendremos la siguiente salida:

{
  "first_name": "Rodrigo",
  "last_name": "Lopez",
  "age": 21,
  "city": "string",
  "state": "string",
  "country": "string"
}

Ahora ya no aparece, ni el campo “adicional”, ni todo el modelo, a pesar de que nuestro diccionario es más grande y es lo que estamos regresando un diccionario, no un modelo.
Como nota adicional, es importante cubrir los campos obligatorios del response_model, puesto que de no hacerlo la respuesta del servidor sera de tipo 500, en caso de que reconstruyas otro objeto por aparte.

Refactorización y modularización

Nota importante: JAMAS debemos enviar la contraseña a un cliente. Ni almacenarla en texto plano, sino en un hash.

Response model es un atributo de nuestra path operation.

Otra solución es tener una clase BasePerson sin la contraseña y otra clase Person que herede de BasePerson y además se le agregue la contraseña así:

class BasePerson(BaseModel):
	# Atributos normales

class Person(BasePerson):
    password: SecretStr = Field(..., title="Password", min_length=8)

@app.post("/person/new", response_model=BasePerson)
def create_person(person: Person = Body(..., embed=True)):
    return person

Lo usé para ocultar el password y la tarjeta de crédito:

class PersonOut(BaseModel): # Response model (protect pasword & credit card)
    first_name: str = Field(
        ...,
        min_length=3,
        max_length=35,
        example="Tony"
        )
    last_name: str = Field(
        ...,
        min_length=3,
        max_length=35,
        example="Demarkovich"
        )
    age_person: int = Field(
        ...,
        ge=18,
        le=115,
        example=39
        )
    color_hair: Optional[HairColor] = Field(default=None, example=HairColor.black)
    married: Optional[bool] = Field(default=None, example=True)
    email_usr : EmailStr = Field(default=None, example="[email protected]")
    web_usr : HttpUrl = Field(default=None, example="")

De esta manera se oculta esa información:

Hablando de Tipos de Datos Exoticos y despues de indagar un poco en la documentación de Pydantic, tambien existe un tipo de dato llamado SecretStr, puedes verlo aquí, que nos permite esconder de manera visual los datos enviados de un string, y se utilizaría de la misma forma que utilizamos el tipo de dato EmailStr que usamos en el primer curso de FastAPI.

Quedaría de la siguiente forma:

from pydantic import BaseModel, Field, EmailStr, SecretStr

class Person(BaseModel):
    first_name: str = Field(
        ...,
        min_length=1,
        max_length=50
    )
    last_name: str = Field(
        ...,
        min_length=1,
        max_length=50
    )
    age: int = Field(
        ...,
        gt= 0,
        le= 100
    )
    hair_color: Optional[HairColor] = Field(default= None)
    is_married: Optional[bool] = Field(default=None)
    email: EmailStr = Field(...)
    password: SecretStr = Field(..., min_length=5)
    class Config:
        schema_extra = {
            "example": {
                "first_name": "Illich",
                "last_name": "Rada",
                "age": 21,
                "hair_color": "blonde",
                "is_married": False,
                "email": "[email protected]",
                "password": "12345UrlIllich"
            }
        }

Y al momento de verificarlo en la documentación de Swagger se ve asi:

{
  "first_name": "Illich",
  "last_name": "Rada",
  "age": 21,
  "hair_color": "blonde",
  "is_married": false,
  "email": "[email protected]",
  "password": "**********"
}

Puede que siga siendo una mejor solución crear un modelo nuevo excluyendo los valores que no queremos mostrar pero me parece que puede ser util para otro tipo de dato sensible que no queremos que quede de forma visual en la traza 😃