El parámetro de query q es de tipo str | None, eso significa que es de tipo str pero también podría ser None, y de hecho, el valor por defecto es None, así que FastAPI sabrá que no es requerido.
Nota
FastAPI sabrá que el valor de q no es requerido por el valor por defecto = None.
Tener str | None permitirá que tu editor te dé mejor soporte y detecte errores.
Ahora que tenemos este Annotated donde podemos poner más información (en este caso alguna validación adicional), añade Query dentro de Annotated, y configura el parámetro max_length a 50:
Ten en cuenta que el valor por defecto sigue siendo None, así que el parámetro sigue siendo opcional.
Pero ahora, teniendo Query(max_length=50) dentro de Annotated, le estamos diciendo a FastAPI que queremos que tenga validación adicional para este valor, queremos que tenga máximo 50 caracteres. 😎
Consejo
Aquí estamos usando Query() porque este es un parámetro de query. Más adelante veremos otros como Path(), Body(), Header(), y Cookie(), que también aceptan los mismos argumentos que Query().
FastAPI ahora:
Validar los datos asegurando que la longitud máxima sea de 50 caracteres
Mostrar un error claro para el cliente cuando los datos no son válidos
Documentar el parámetro en el esquema OpenAPI de la path operation (para que aparezca en la documentación automática UI)
Alternativa (antigua): Query como valor por defecto¶
Versiones anteriores de FastAPI (antes de 0.95.0) requerían que usaras Query como valor por defecto de tu parámetro, en lugar de ponerlo en Annotated, hay una alta probabilidad de que veas código usándolo por ahí, así que te lo explicaré.
Consejo
Para código nuevo y siempre que sea posible, usa Annotated como se explicó arriba. Hay múltiples ventajas (explicadas abajo) y ninguna desventaja. 🍰
Así es como usarías Query() como valor por defecto de tu parámetro de función, configurando el parámetro max_length a 50:
Como en este caso (sin usar Annotated) tenemos que reemplazar el valor por defecto None en la función con Query(), ahora necesitamos configurar el valor por defecto con el parámetro Query(default=None), sirve para el mismo propósito de definir ese valor por defecto (al menos para FastAPI).
Así que:
q:str|None=Query(default=None)
...hace el parámetro opcional, con un valor por defecto de None, igual que:
q:str|None=None
Pero la versión con Query lo declara explícitamente como un parámetro de query.
Luego, podemos pasar más parámetros a Query. En este caso, el parámetro max_length que aplica a strings:
q:str|None=Query(default=None,max_length=50)
Esto validará los datos, mostrará un error claro cuando los datos no sean válidos, y documentará el parámetro en el esquema OpenAPI de la path operation.
Usar Annotated es lo recomendado en lugar del valor por defecto en los parámetros de función, es mejor por múltiples razones. 🤓
El valor por defecto del parámetro de función es el valor por defecto real, eso es más intuitivo con Python en general. 😌
Podrías llamar a esa misma función en otros lugares sin FastAPI, y funcionaría como se espera. Si hay un parámetro requerido (sin un valor por defecto), tu editor te avisará con un error, Python también se quejará si lo ejecutas sin pasar el parámetro requerido.
Cuando no usas Annotated y en su lugar usas el estilo (antiguo) de valor por defecto, si llamas a esa función sin FastAPI en otros lugares, tienes que recordar pasar los argumentos a la función para que funcione correctamente, de lo contrario los valores serán diferentes de lo que esperas (ej. QueryInfo o algo similar en lugar de str). Y tu editor no se quejará, y Python no se quejará al ejecutar esa función, solo cuando las operaciones internas den error.
Como Annotated puede tener más de una anotación de metadatos, ahora podrías incluso usar la misma función con otras herramientas, como Typer. 🚀
Este patrón de expresión regular específico verifica que el valor del parámetro recibido:
^: empieza con los siguientes caracteres, no tiene caracteres antes.
fixedquery: tiene el valor exacto fixedquery.
$: termina ahí, no tiene más caracteres después de fixedquery.
Si te sientes perdido con todas estas ideas de "expresión regular", no te preocupes. Son un tema difícil para muchas personas. Todavía puedes hacer muchas cosas sin necesitar expresiones regulares.
Ahora sabes que cuando las necesites puedes usarlas en FastAPI.
Cuando no necesitamos declarar más validaciones o metadatos, podemos hacer que el parámetro de query q sea requerido simplemente no declarando un valor por defecto, así:
q:str
en lugar de:
q:str|None=None
Pero ahora lo estamos declarando con Query, por ejemplo así:
q:Annotated[str|None,Query(min_length=3)]=None
Así que, cuando necesites declarar un valor como requerido mientras usas Query, puedes simplemente no declarar un valor por defecto:
Puedes declarar que un parámetro puede aceptar None, pero que sigue siendo requerido. Esto forzaría a los clientes a enviar un valor, incluso si el valor es None.
Para hacer eso, puedes declarar que None es un tipo válido pero simplemente no declarar un valor por defecto:
Cuando defines un parámetro de query explícitamente con Query también puedes declararlo para recibir una lista de valores, o dicho de otra manera, para recibir múltiples valores.
Por ejemplo, para declarar un parámetro de query q que puede aparecer múltiples veces en la URL, puedes escribir:
recibirías los múltiples valores de los parámetros de queryq (foo y bar) en una list de Python dentro de tu función de path operation, en el parámetro de funciónq.
Así, la respuesta a esa URL sería:
{"q":["foo","bar"]}
Consejo
Para declarar un parámetro de query con un tipo list, como en el ejemplo anterior, necesitas usar explícitamente Query, de lo contrario sería interpretado como un cuerpo de request.
La documentación interactiva de la API se actualizará en consecuencia, para permitir múltiples valores:
Lista de parámetros de query / múltiples valores con valores por defecto¶
También puedes definir una list de valores por defecto si no se proporciona ninguno:
Esa información será incluida en el OpenAPI generado y usada por las interfaces de usuario de la documentación y herramientas externas.
Nota
Ten en cuenta que diferentes herramientas pueden tener diferentes niveles de soporte para OpenAPI.
Algunas podrían no mostrar toda la información extra declarada todavía, aunque en la mayoría de los casos, la característica faltante ya está planificada para desarrollo.
fromtypingimportAnnotatedfromfastapiimportFastAPI,Queryapp=FastAPI()@app.get("/items/")asyncdefread_items(q:Annotated[str|None,Query(title="Query string",description="Query string for the items to search in the database that have a good match",min_length=3,),]=None,):results={"items":[{"item_id":"Foo"},{"item_id":"Bar"}]}ifq:results.update({"q":q})returnresults
🤓 Otras versiones y variantes
Consejo
Preferible usar la versión con Annotated si es posible.
fromfastapiimportFastAPI,Queryapp=FastAPI()@app.get("/items/")asyncdefread_items(q:str|None=Query(default=None,title="Query string",description="Query string for the items to search in the database that have a good match",min_length=3,),):results={"items":[{"item_id":"Foo"},{"item_id":"Bar"}]}ifq:results.update({"q":q})returnresults
Tienes que dejarlo ahí un tiempo porque hay clientes usándolo, pero quieres que la documentación lo muestre claramente como deprecado.
Entonces pasa el parámetro deprecated=True a Query:
fromtypingimportAnnotatedfromfastapiimportFastAPI,Queryapp=FastAPI()@app.get("/items/")asyncdefread_items(q:Annotated[str|None,Query(alias="item-query",title="Query string",description="Query string for the items to search in the database that have a good match",min_length=3,max_length=50,pattern="^fixedquery$",deprecated=True,),]=None,):results={"items":[{"item_id":"Foo"},{"item_id":"Bar"}]}ifq:results.update({"q":q})returnresults
🤓 Otras versiones y variantes
Consejo
Preferible usar la versión con Annotated si es posible.
fromfastapiimportFastAPI,Queryapp=FastAPI()@app.get("/items/")asyncdefread_items(q:str|None=Query(default=None,alias="item-query",title="Query string",description="Query string for the items to search in the database that have a good match",min_length=3,max_length=50,pattern="^fixedquery$",deprecated=True,),):results={"items":[{"item_id":"Foo"},{"item_id":"Bar"}]}ifq:results.update({"q":q})returnresults
Para excluir un parámetro de query del esquema OpenAPI generado (y por ende, de los sistemas de documentación automática), configura el parámetro include_in_schema de Query a False:
Podría haber casos donde necesites hacer alguna validación personalizada que no se pueda hacer con los parámetros mostrados arriba.
En esos casos, puedes usar una función validadora personalizada que se aplica después de la validación normal (ej. después de validar que el valor es un str).
Por ejemplo, este validador personalizado verifica que el ID del item empiece con isbn- para un número de libro ISBN o con imdb- para un ID de URL de película IMDB:
importrandomfromtypingimportAnnotatedfromfastapiimportFastAPIfrompydanticimportAfterValidatorapp=FastAPI()data={"isbn-9781529046137":"The Hitchhiker's Guide to the Galaxy","imdb-tt0371724":"The Hitchhiker's Guide to the Galaxy","isbn-9781439512982":"Isaac Asimov: The Complete Stories, Vol. 2",}defcheck_valid_id(id:str):ifnotid.startswith(("isbn-","imdb-")):raiseValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')returnid@app.get("/items/")asyncdefread_items(id:Annotated[str|None,AfterValidator(check_valid_id)]=None,):ifid:item=data.get(id)else:id,item=random.choice(list(data.items()))return{"id":id,"name":item}
Nota
Esto está disponible con Pydantic versión 2 o superior. 😎
Consejo
Si necesitas hacer cualquier tipo de validación que requiera comunicarse con algún componente externo, como una base de datos u otra API, deberías usar Dependencias de FastAPI, aprenderás sobre ellas más adelante.
Estos validadores personalizados son para cosas que se pueden verificar con únicamente los mismos datos proporcionados en la request.
¿Lo notaste? Un string usando value.startswith() puede tomar una tupla, y verificará cada valor en la tupla:
# Code above omitted 👆defcheck_valid_id(id:str):ifnotid.startswith(("isbn-","imdb-")):raiseValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')returnid# Code below omitted 👇
👀 Vista previa del archivo completo
importrandomfromtypingimportAnnotatedfromfastapiimportFastAPIfrompydanticimportAfterValidatorapp=FastAPI()data={"isbn-9781529046137":"The Hitchhiker's Guide to the Galaxy","imdb-tt0371724":"The Hitchhiker's Guide to the Galaxy","isbn-9781439512982":"Isaac Asimov: The Complete Stories, Vol. 2",}defcheck_valid_id(id:str):ifnotid.startswith(("isbn-","imdb-")):raiseValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')returnid@app.get("/items/")asyncdefread_items(id:Annotated[str|None,AfterValidator(check_valid_id)]=None,):ifid:item=data.get(id)else:id,item=random.choice(list(data.items()))return{"id":id,"name":item}
Con data.items() obtenemos un objeto iterable con tuplas que contienen la clave y el valor de cada item del diccionario.
Convertimos este objeto iterable en una list propiamente dicha con list(data.items()).
Luego con random.choice() podemos obtener un valor aleatorio de la lista, así que, obtenemos una tupla con (id, name). Será algo como ("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy").
Luego asignamos esos dos valores de la tupla a las variables id y name.
Así, si el usuario no proporcionó un ID de item, seguirá recibiendo una sugerencia aleatoria.
...hacemos todo esto en una sola línea simple. 🤯 ¿No amas Python? 🐍
importrandomfromtypingimportAnnotatedfromfastapiimportFastAPIfrompydanticimportAfterValidatorapp=FastAPI()data={"isbn-9781529046137":"The Hitchhiker's Guide to the Galaxy","imdb-tt0371724":"The Hitchhiker's Guide to the Galaxy","isbn-9781439512982":"Isaac Asimov: The Complete Stories, Vol. 2",}defcheck_valid_id(id:str):ifnotid.startswith(("isbn-","imdb-")):raiseValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')returnid@app.get("/items/")asyncdefread_items(id:Annotated[str|None,AfterValidator(check_valid_id)]=None,):ifid:item=data.get(id)else:id,item=random.choice(list(data.items()))return{"id":id,"name":item}