¿Cómo agregar tanto el archivo como el cuerpo JSON en una solicitud POST FastAPI?
Específicamente, quiero que funcione el siguiente ejemplo:
from typing import List
from pydantic import BaseModel
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
class DataConfiguration(BaseModel):
textColumnNames: List[str]
idColumn: str
@app.post("/data")
async def data(dataConfiguration: DataConfiguration,
csvFile: UploadFile = File(...)):
pass
# read requested id and text columns from csvFile
Si esta no es la forma adecuada de realizar una POST
solicitud, hágame saber cómo seleccionar las columnas requeridas de un archivo CSV cargado en FastAPI.
Según la documentación de FastAPI :
Puede declarar múltiples
Form
parámetros en una operación de ruta, pero no puede declarar tambiénBody
los campos que espera recibir comoJSON
, ya que la solicitud tendrá el cuerpo codificado usandoapplication/x-www-form-urlencoded
en lugar deapplication/json
(cuando el formulario incluye archivos, se codifica comomultipart/form-data
).Esto no es una limitación de FastAPI, es parte del
HTTP
protocolo.
Tenga en cuenta que primero debe tenerlo python-multipart
instalado (si aún no lo ha hecho), ya que los archivos cargados se envían como "datos de formulario" . Por ejemplo:
pip install python-multipart
También cabe señalar que en los ejemplos siguientes, los puntos finales se definen con normal def
, pero también puedes usarlos async def
(según tus necesidades). Eche un vistazo a esta respuesta para obtener más detalles sobre def
vs async def
en FastAPI.
Si está buscando cómo cargar archivos y list
diccionarios/datos JSON , eche un vistazo a esta respuesta , así como a esta respuesta y esta respuesta para ver ejemplos de trabajo (que se basan principalmente en algunos de los siguientes métodos).
Método 1
Como se describe aquí , se pueden definir archivos y formar archivos al mismo tiempo usando File
y Form
. A continuación se muestra un ejemplo práctico. En caso de que tenga una gran cantidad de parámetros y desee definirlos por separado del punto final , consulte esta respuesta sobre cómo crear una clase de dependencia personalizada.
aplicación.py
from fastapi import Form, File, UploadFile, Request, FastAPI
from typing import List
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.post("/submit")
def submit(
name: str = Form(...),
point: float = Form(...),
is_accepted: bool = Form(...),
files: List[UploadFile] = File(...),
):
return {
"JSON Payload": {"name": name, "point": point, "is_accepted": is_accepted},
"Filenames": [file.filename for file in files],
}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
Puede probar el ejemplo anterior accediendo a la plantilla siguiente en http://127.0.0.1:8000
. Si su plantilla no incluye ningún código Jinja, también puede devolver un archivo HTMLResponse
.
plantillas/ index.html
<!DOCTYPE html>
<html>
<body>
<form method="post" action="http://127.0.0.1:8000/submit" enctype="multipart/form-data">
name : <input type="text" name="name" value="foo"><br>
point : <input type="text" name="point" value=0.134><br>
is_accepted : <input type="text" name="is_accepted" value=True><br>
<label for="file">Choose files to upload</label>
<input type="file" id="files" name="files" multiple>
<input type="submit" value="submit">
</form>
</body>
</html>
También puede probar este ejemplo usando los autodocs interactivos de OpenAPI/Swagger UI en /docs
, por ejemplo, http://127.0.0.1:8000/docs
o usando Python requests
, como se muestra a continuación:
prueba.py
import requests
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
Método 2
También se podrían usar modelos Pydantic, junto con Dependencias , para informar al /submit
punto final (en el ejemplo siguiente) que la variable parametrizada base
depende de la Base
clase. Tenga en cuenta que este método espera los base
datos como query
( no body
) parámetros, que luego se validan y se convierten al modelo Pydantic (en este caso, es decir, el Base
modelo). La devolución de una instancia de modelo Pydantic (en este caso, base
) desde un punto final FastAPI se convertiría automáticamente en el diccionario/objeto JSON equivalente detrás de escena, utilizando jsonable_encoder
, como se explica en detalle en esta respuesta . Sin embargo, si desea que esto lo haga usted mismo dentro del punto final, puede usar model_dump()
el método de Pydantic, por ejemplo, base.model_dump()
o simplemente dict(base)
, como se explica en esta respuesta . Además data
, el siguiente ejemplo espera Files
lo mismo que multipart/form-data
en el cuerpo de la solicitud.
aplicación.py
from fastapi import Form, File, UploadFile, Request, FastAPI, Depends
from typing import List
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from typing import Optional
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
@app.post("/submit")
def submit(base: Base = Depends(), files: List[UploadFile] = File(...)):
return {
"JSON Payload": base,
"Filenames": [file.filename for file in files],
}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
Nuevamente, puedes probarlo usando la plantilla a continuación, que, esta vez, usa JavaScript para modificar el action
atributo del form
elemento, con el fin de pasar los form
datos como query
parámetros a la URL en lugar de form-data
.
plantillas/ index.html
<!DOCTYPE html>
<html>
<body>
<form method="post" id="myForm" onclick="transformFormData();" enctype="multipart/form-data">
name : <input type="text" name="name" value="foo"><br>
point : <input type="text" name="point" value=0.134><br>
is_accepted : <input type="text" name="is_accepted" value=True><br>
<label for="file">Choose files to upload</label>
<input type="file" id="files" name="files" multiple>
<input type="submit" value="submit">
</form>
<script>
function transformFormData(){
var myForm = document.getElementById('myForm');
var qs = new URLSearchParams(new FormData(myForm)).toString();
myForm.action = 'http://127.0.0.1:8000/submit?' + qs;
}
</script>
</body>
</html>
Como se mencionó anteriormente, para probar la API, también puede usar Swagger UI o Python requests
, como se muestra en el siguiente ejemplo. Tenga en cuenta que los datos ahora deben pasarse al argumento params
( no al data
) del requests.post()
método, ya que los datos ahora se envían como query
parámetros, no form-data
en el cuerpo de la solicitud, como era el caso en el Método 1 anterior.
prueba.py
import requests
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
params = {"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, params=params, files=files)
print(resp.json())
Método 3
Otra opción sería pasar los datos del cuerpo como un único parámetro (de tipo Form
) en forma de cadena JSON. Para ello, necesitaría crear una función de dependencia en el lado del servidor.
Una dependencia es "simplemente una función que puede tomar los mismos parámetros que una función de operación de ruta (también conocida como punto final ). Puede considerarla como una función de operación de ruta sin el decorador" . Por lo tanto, deberá declarar la dependencia de la misma manera que lo haría con los parámetros de su punto final (es decir, los nombres y tipos de los parámetros en la dependencia deben ser los que FastAPI espera cuando un cliente envía una solicitud HTTP a ese punto final). Por ejemplo, data: str = Form(...)
). Luego, cree un nuevo parámetro (por ejemplo, base
) en su punto final, usando Depends()
y pasándole la función de dependencia como parámetro ( Nota: no lo llame directamente, es decir, no agregue paréntesis al final del nombre de su función. pero en su lugar use, por ejemplo, Depends(checker)
donde checker
está el nombre de su función de dependencia). Cada vez que llega una nueva solicitud, FastAPI se encargará de llamar a su dependencia, obtener el resultado y asignar ese resultado al parámetro (por ejemplo, base
) en su punto final. Para obtener más detalles sobre las dependencias, consulte los enlaces proporcionados en esta sección.
En este caso, la función de dependencia debe usarse para analizar la (cadena JSON) data
usando el parse_raw
método ( Nota : en Pydantic V2 parse_raw
ha quedado obsoleto y reemplazado por model_validate_json
), así como para validarlo data
con el modelo de Pydantic correspondiente. Si ValidationError
se genera, HTTP_422_UNPROCESSABLE_ENTITY
se debe enviar un error al cliente, incluido el mensaje de error; de lo contrario, se asigna una instancia de ese modelo (es decir, Base
modelo, en este caso) al parámetro en el punto final, que podría usarse como se desee. A continuación se da un ejemplo:
aplicación.py
from fastapi import FastAPI, status, Form, UploadFile, File, Depends, Request
from pydantic import BaseModel, ValidationError
from fastapi.exceptions import HTTPException
from fastapi.encoders import jsonable_encoder
from typing import Optional, List
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
def checker(data: str = Form(...)):
try:
return Base.model_validate_json(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
@app.post("/submit")
def submit(base: Base = Depends(checker), files: List[UploadFile] = File(...)):
return {"JSON Payload": base, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
Checker
Clase de dependencia genérica
En caso de que tuviera varios modelos y quisiera evitar crear una checker
función para cada modelo, podría crear una clase de dependencia genérica , como se describe en la documentación (consulte esta respuesta para obtener más detalles también) y usarla para cada modelo diferente. en su API. Ejemplo:Checker
# ... rest of the code is the same as above
class Other(BaseModel):
msg: str
details: Base
class Checker:
def __init__(self, model: BaseModel):
self.model = model
def __call__(self, data: str = Form(...)):
try:
return self.model.model_validate_json(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
@app.post("/submit")
def submit(base: Base = Depends(Checker(Base)), files: List[UploadFile] = File(...)):
pass
@app.post("/submit_other")
def submit_other(other: Other = Depends(Checker(Other)), files: List[UploadFile] = File(...)):
pass
Datos JSON arbitrarios
En caso de que validar los datos de entrada con un modelo Pydantic específico no fuera importante para usted, pero, en cambio, le gustaría recibir datos JSON arbitrarios y simplemente verificar si el cliente envió o no una cadena JSON válida, puede usar lo siguiente :
# ...
from json import JSONDecodeError
import json
def checker(data: str = Form(...)):
try:
return json.loads(data)
except JSONDecodeError:
raise HTTPException(status_code=400, detail='Invalid JSON data')
@app.post("/submit")
def submit(payload: dict = Depends(checker), files: List[UploadFile] = File(...)):
pass
Alternativamente, puedes simplemente usar el Json
tipo de Pydantic (como se muestra aquí ):
from pydantic import Json
@app.post("/submit")
def submit(data: Json = Form(), files: List[UploadFile] = File(...)):
pass
Prueba usando Pythonrequests
prueba.py
Tenga en cuenta que en JSON
, los valores booleanos se representan utilizando literales true
o false
en minúsculas, mientras que en Python deben escribirse en mayúscula como True
o False
.
import requests
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {'data': '{"name": "foo", "point": 0.13, "is_accepted": false}'}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
O, si lo prefieres:
import requests
import json
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {'data': json.dumps({"name": "foo", "point": 0.13, "is_accepted": False})}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
PD: Para probar el /submit_other
punto final (descrito en la Checker
clase genérica anterior) usando Python requests
, reemplace el data
atributo en el ejemplo anterior con el siguiente:
import requests
import json
url = 'http://127.0.0.1:8000/submit_other'
data = {'data': json.dumps({"msg": "Hi", "details": {"name": "bar", "point": 0.11, "is_accepted": True}})}
# ... rest of the code is the same as above
Pruebe usando Fetch API o Axios
plantillas/ index.html
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js"></script>
</head>
<body>
<input type="file" id="fileInput" name="file" onchange="reset()" multiple><br>
<input type="button" value="Submit using fetch" onclick="submitUsingFetch()">
<input type="button" value="Submit using axios" onclick="submitUsingAxios()">
<p id="resp"></p>
<script>
function reset() {
var resp = document.getElementById("resp");
resp.innerHTML = "";
resp.style.color = "black";
}
function submitUsingFetch() {
var resp = document.getElementById("resp");
var fileInput = document.getElementById('fileInput');
if (fileInput.files[0]) {
var formData = new FormData();
formData.append("data", JSON.stringify({"name": "foo", "point": 0.13, "is_accepted": false}));
for (const file of fileInput.files)
formData.append('files', file);
fetch('/submit', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(data => {
resp.innerHTML = JSON.stringify(data); // data is a JSON object
})
.catch(error => {
console.error(error);
});
} else {
resp.innerHTML = "Please choose some file(s)...";
resp.style.color = "red";
}
}
function submitUsingAxios() {
var resp = document.getElementById("resp");
var fileInput = document.getElementById('fileInput');
if (fileInput.files[0]) {
var formData = new FormData();
formData.append("data", JSON.stringify({"name": "foo", "point": 0.13, "is_accepted": false}));
for (const file of fileInput.files)
formData.append('files', file);
axios({
method: 'POST',
url: '/submit',
data: formData,
})
.then(response => {
resp.innerHTML = JSON.stringify(response.data); // response.data is a JSON object
})
.catch(error => {
console.error(error);
});
} else {
resp.innerHTML = "Please choose some file(s)...";
resp.style.color = "red";
}
}
</script>
</body>
</html>
Método 4
Otro método proviene de la discusión de github aquí e incorpora una clase personalizada con un método de clase usado para transformar una JSON
cadena dada en un diccionario de Python, que luego se usa para la validación contra el modelo Pydantic ( tenga en cuenta que, en comparación con el ejemplo dado en el enlace de github antes mencionado, el siguiente ejemplo utiliza @model_validator(mode='before')
, desde la introducción de Pydantic V2).
De manera similar al método 3 anterior, los datos de entrada deben pasarse como un único Form
parámetro en forma de JSON
cadena (tenga en cuenta que definir el data
parámetro en el ejemplo siguiente con Body
o Form
funcionaría independientemente: Form
es una clase que hereda directamente deBody
. Es decir, FastAPI Todavía esperaríamos la cadena JSON como form
datos, no como application/json
, ya que en este caso la solicitud tendrá el cuerpo codificado usando multipart/form-data
). Por lo tanto, los mismos ejemplos de test.py y la plantilla index.html del Método 3 anterior también podrían usarse para probar el siguiente ejemplo.
aplicación.py
from fastapi import FastAPI, File, Body, UploadFile, Request
from pydantic import BaseModel, model_validator
from typing import Optional, List
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
import json
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
@model_validator(mode='before')
@classmethod
def validate_to_json(cls, value):
if isinstance(value, str):
return cls(**json.loads(value))
return value
@app.post("/submit")
def submit(data: Base = Body(...), files: List[UploadFile] = File(...)):
return {"JSON Payload": data, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
Método 5
Otra solución sería convertir los bytes del archivo en una base64
cadena de formato y agregarlos al objeto JSON, junto con otros datos que quizás desee enviar al servidor. No recomendaría encarecidamente utilizar este enfoque; sin embargo, se ha agregado a esta respuesta como una opción adicional, en aras de la integridad.
La razón por la que no sugeriría su uso es porque codificar los archivos base64
esencialmente aumentaría el tamaño del archivo y, por lo tanto, aumentaría la utilización del ancho de banda, así como el tiempo y los recursos (por ejemplo, el uso de la CPU) necesarios para cargar el archivo. (especialmente, cuando varios usuarios van a utilizar la API al mismo tiempo), ya que la codificación y decodificación base64 tendrían que realizarse en el lado del cliente y del servidor, respectivamente (este enfoque solo podría ser útil para imágenes muy pequeñas). Según la documentación de MDN :
Cada dígito Base64 representa exactamente 6 bits de datos. Por lo tanto, tres bytes de 8 bits de la cadena de entrada/archivo binario (3 × 8 bits = 24 bits) se pueden representar mediante cuatro dígitos Base64 de 6 bits (4 × 6 = 24 bits).
Esto significa que la versión Base64 de una cadena o archivo tendrá al menos un 133% del tamaño de su fuente (un aumento de ~33% ). El aumento puede ser mayor si los datos codificados son pequeños. Por ejemplo, la cadena
"a"
conlength === 1
se codifica"YQ=="
conlength === 4
: un aumento del 300% .
Usando este enfoque, que nuevamente no recomendaría por las razones discutidas anteriormente, necesitaría asegurarse de definir el punto final con normal def
, ya que base64.b64decode()
realiza una operación de bloqueo que bloquearía el bucle de eventos y, por lo tanto, todo el servidor. Eche un vistazo. en esta respuesta para más detalles. De lo contrario, para usar async def
el punto final, debe ejecutar la función de decodificación en un dispositivo externo ThreadPool
o ProcessPool
(nuevamente, consulte esta respuesta sobre cómo hacerlo), así como usarla aiofiles
para escribir el archivo en el disco (consulte también esta respuesta ).
requests
El siguiente ejemplo también proporciona ejemplos de pruebas de clientes en Python y JavaScript.
aplicación.py
from fastapi import FastAPI, Request, HTTPException
from typing import List
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from fastapi.templating import Jinja2Templates
import base64
import binascii
app = FastAPI()
templates = Jinja2Templates(directory='templates')
class Bas64File(BaseModel):
filename: str
owner: str
bas64_str: str
@app.post('/submit')
def submit(files: List[Bas64File]):
for file in files:
try:
contents = base64.b64decode(file.bas64_str.encode('utf-8'))
with open(file.filename, 'wb') as f:
f.write(contents)
except base64.binascii.Error as e:
raise HTTPException(
400, detail='There was an error decoding the base64 string'
)
except Exception:
raise HTTPException(
500, detail='There was an error uploading the file(s)'
)
return {'Filenames': [file.filename for file in files]}
@app.get('/', response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse('index.html', {'request': request})
Prueba usando Pythonrequests
prueba.py
import requests
import os
import glob
import base64
url = 'http://127.0.0.1:8000/submit'
paths = glob.glob('files/*', recursive=True)
payload = []
for p in paths:
with open(p, 'rb') as f:
bas64_str = base64.b64encode(f.read()).decode('utf-8')
payload.append({'filename': os.path.basename(p), 'owner': 'me', 'bas64_str': bas64_str})
resp = requests.post(url=url, json=payload)
print(resp.json())
Prueba usando Fetch API
plantillas/ index.html
<input type="file" id="fileInput" onchange="base64Handler()" multiple><br>
<script>
async function base64Handler() {
var fileInput = document.getElementById('fileInput');
var payload = [];
for (const file of fileInput.files) {
var dict = {};
dict.filename = file.name;
dict.owner = 'me';
base64String = await this.toBase64(file);
dict.bas64_str = base64String.replace("data:", "").replace(/^.+,/, "");
payload.push(dict);
}
uploadFiles(payload);
}
function toBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
};
function uploadFiles(payload) {
fetch('/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => {
console.log(response);
})
.catch(error => {
console.error(error);
});
}
</script>
No puedes mezclar datos de formulario con json.
Según la documentación de FastAPI :
Advertencia: Puede declarar varios parámetros
File
yForm
en una operación de ruta, pero tampoco puede declararBody
los campos que espera recibir como JSON, ya que la solicitud tendrá el cuerpo codificado usandomultipart/form-data
en lugar deapplication/json
. Esto no es una limitación de FastAPI, es parte del protocolo HTTP.
Sin embargo, puede utilizar Form(...)
como solución adjuntar una cadena adicional como form-data
:
from typing import List
from fastapi import FastAPI, UploadFile, File, Form
app = FastAPI()
@app.post("/data")
async def data(textColumnNames: List[str] = Form(...),
idColumn: str = Form(...),
csvFile: UploadFile = File(...)):
pass
Opté por el muy elegante Método 3 de @Chris (propuesto originalmente por @M.Winkwns). Sin embargo, lo modifiqué ligeramente para que funcione con cualquier modelo de Pydantic:
from typing import Type, TypeVar
from pydantic import BaseModel, ValidationError
from fastapi import Form
Serialized = TypeVar("Serialized", bound=BaseModel)
def form_json_deserializer(schema: Type[Serialized], data: str = Form(...)) -> Serialized:
"""
Helper to serialize request data not automatically included in an application/json body but
within somewhere else like a form parameter. This makes an assumption that the form parameter with JSON data is called 'data'
:param schema: Pydantic model to serialize into
:param data: raw str data representing the Pydantic model
:raises ValidationError: if there are errors parsing the given 'data' into the given 'schema'
"""
try:
return schema.parse_raw(data)
except ValidationError as e
raise HTTPException(detail=jsonable_encoder(e.errors()), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
Cuando lo usa en un punto final, puede usarlo functools.partial
para vincular el modelo Pydantic específico:
import functools
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
@app.post("/upload")
async def upload(
data: Base = Depends(functools.partial(form_json_deserializer, Base)),
files: Sequence[UploadFile] = File(...)
) -> Base:
return data
Como lo indica @Chris (y solo para completar):
Según la documentación de FastAPI,
Puede declarar múltiples parámetros de formulario en una operación de ruta, pero no puede declarar también los campos de cuerpo que espera recibir como JSON, ya que la solicitud tendrá el cuerpo codificado usando application/x-www-form-urlencoded en lugar de application/ json. (Pero cuando el formulario incluye archivos, se codifica como datos de formulario/multiparte)
Esto no es una limitación de FastAPI, es parte del protocolo HTTP.
Como su Método1 no era una opción y el Método2 no puede funcionar para tipos de datos profundamente anidados, se me ocurrió una solución diferente:
Simplemente convierta su tipo de datos a una cadena/json y llame a parse_raw
la función pydantics
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
@app.post("/submit")
async def submit(base: str = Form(...), files: List[UploadFile] = File(...)):
try:
model = Base.parse_raw(base)
except pydantic.ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
) from e
return {"JSON Payload ": received_data, "Uploaded Filenames": [file.filename for file in files]}
Si estás usando pydantic v2
:
import json
@app.post(/endpoint)
async def endpoint(file: UploadFile, payload: A)
class A(BaseModel):
attr: str
@model_validator(mode="before")
@classmethod
def to_py_dict(cls, data):
return json.loads(data)
Su solicitud será multipart/form-data , el valor de la clave de carga útil será una cadena en formato JSON , y cuando llegue a la etapa de serialización del modelo , @model_validator se ejecutará antes de eso, y luego podrá transformar el valor en un Python. dict y devolverlo a la serialización.