¿Cómo agregar tanto el archivo como el cuerpo JSON en una solicitud POST FastAPI?

Resuelto Abdullah asked hace 3 años • 0 respuestas

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 POSTsolicitud, hágame saber cómo seleccionar las columnas requeridas de un archivo CSV cargado en FastAPI.

Abdullah avatar Dec 30 '20 16:12 Abdullah
Aceptado

Según la documentación de FastAPI :

Puede declarar múltiples Formparámetros en una operación de ruta, pero no puede declarar también Bodylos campos que espera recibir comoJSON , ya que la solicitud tendrá el cuerpo codificado usando application/x-www-form-urlencodeden lugar de application/json(cuando el formulario incluye archivos, se codifica como multipart/form-data).

Esto no es una limitación de FastAPI, es parte del HTTPprotocolo.

Tenga en cuenta que primero debe tenerlo python-multipartinstalado (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 defvs async defen FastAPI.

Si está buscando cómo cargar archivos y listdiccionarios/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 Filey 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/docso 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 /submitpunto final (en el ejemplo siguiente) que la variable parametrizada basedepende de la Baseclase. Tenga en cuenta que este método espera los basedatos como query( no body ) parámetros, que luego se validan y se convierten al modelo Pydantic (en este caso, es decir, el Basemodelo). 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 Fileslo mismo que multipart/form-dataen 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 actionatributo del formelemento, con el fin de pasar los formdatos como querypará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 queryparámetros, no form-dataen 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 checkerestá 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) datausando el parse_rawmétodo ( Nota : en Pydantic V2 parse_rawha quedado obsoleto y reemplazado por model_validate_json), así como para validarlo datacon el modelo de Pydantic correspondiente. Si ValidationErrorse genera, HTTP_422_UNPROCESSABLE_ENTITYse debe enviar un error al cliente, incluido el mensaje de error; de lo contrario, se asigna una instancia de ese modelo (es decir, Basemodelo, 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})
CheckerClase de dependencia genérica

En caso de que tuviera varios modelos y quisiera evitar crear una checkerfunció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 Jsontipo 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 trueo falseen minúsculas, mientras que en Python deben escribirse en mayúscula como Trueo 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_otherpunto final (descrito en la Checkerclase genérica anterior) usando Python requests, reemplace el dataatributo 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 JSONcadena 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 Formparámetro en forma de JSONcadena (tenga en cuenta que definir el dataparámetro en el ejemplo siguiente con Bodyo Formfuncionaría independientemente: Formes una clase que hereda directamente deBody . Es decir, FastAPI Todavía esperaríamos la cadena JSON como formdatos, 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 base64cadena 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 base64esencialmente 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"con length === 1se codifica "YQ=="con length === 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 defel punto final, debe ejecutar la función de decodificación en un dispositivo externo ThreadPoolo ProcessPool(nuevamente, consulte esta respuesta sobre cómo hacerlo), así como usarla aiofilespara escribir el archivo en el disco (consulte también esta respuesta ).

requestsEl 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>
Chris avatar Jan 09 '2022 10:01 Chris

No puedes mezclar datos de formulario con json.

Según la documentación de FastAPI :

Advertencia: Puede declarar varios parámetros Filey Formen una operación de ruta, pero tampoco puede declarar Bodylos campos que espera recibir como JSON, ya que la solicitud tendrá el cuerpo codificado usando multipart/form-dataen 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
Eric L avatar Apr 15 '2021 10:04 Eric L

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.partialpara 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
phillipuniverse avatar Apr 28 '2022 19:04 phillipuniverse

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_rawla 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]}
M.Winkens avatar Feb 22 '2022 13:02 M.Winkens

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.

Vinícius OA avatar Aug 09 '2023 19:08 Vinícius OA