tiangolo/fastapi

Using pydantic Json Type as Form data type doesn't work

Kludex opened this issue · 4 comments

Discussed in #9305

Originally posted by harpaj March 23, 2023

First Check

  • I added a very descriptive title here.
  • I used the GitHub search to find a similar question and didn't find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google "How to X in FastAPI" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to FastAPI but to Pydantic.
  • I already checked if it is not related to FastAPI but to Swagger UI.
  • I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

from typing import Annotated

from fastapi import FastAPI, Form
from pydantic import Json, BaseModel

app = FastAPI()


class JsonListModel(BaseModel):
    json_list: Json[list[str]]


@app.post("/working")
async def working(json_list: Annotated[str, Form()]) -> list[str]:
    model = JsonListModel(json_list=json_list)
    return model.json_list


@app.post("/broken")
async def broken(json_list: Annotated[Json[list[str]], Form()] ) -> list[str]:
    return json_list

Description

In the example code above, I would expect working and broken to be approximately equivalent.
However, while working returns the parsed json_list as expected, broken fails with

{"detail":[{"loc":["body","json_list"],"msg":"JSON object must be str, bytes or bytearray","type":"type_error.json"}]}

Operating System

Linux

Operating System Details

No response

FastAPI Version

0.95.0

Python Version

Python 3.10.8

Additional Context

sample request that fails with broken and works with working:

import requests
import json

headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(
    "http://0.0.0.0:8000/broken",
    data={"json_list": json.dumps(["abc", "def"])},
    headers=headers,
)
```</div>

Hi @Kludex @harpaj
As I've been reviewing this issue, a few questions have arisen.

  1. Is the primary goal of this issue to enable the Form() function to directly receive data in JSON format? Or, is the focus more on providing documentation and examples for handling this indirectly?
  2. Is there a need for detailed documentation on this phenomenon and its causes? Is the goal to provide guidelines to help users understand and appropriately manage this issue?

anybody working on this presently or is open for me to work on ?

Could FastAPI be taking the string 'json_list' and seeing that it is only one string, coercing it into a list by wrapping it?

This seems to work

from typing import Annotated

from fastapi import FastAPI, Form
from pydantic import Json, BaseModel, BeforeValidator
from pydantic_core import from_json

app = FastAPI()


class JsonListModel(BaseModel):
    json_list: Json[list[str]]


@app.post("/working")
async def working(json_list: Annotated[str, Form()]) -> list[str]:
    model = JsonListModel(json_list=json_list)
    return model.json_list

@app.post("/broken")
async def broken(json_list: Annotated[list[str], 
                BeforeValidator(lambda v: from_json(v[0])),
                 Form()]) -> list[str]:
    return json_list

I would have expected from_json[v] to do the trick, but instead I needed to use from_json(v[0])

Edit to add: The test case program works, but the Swagger UI is wrong - it prompts you for a list of strings instead of a single json-encoded-string

I actually thought maybe a little too hard about how to resolve the tension between the backend function "broken" wanting the type of the form-entry after json-decoding, while FastAPI only knows that the form contains a string -- I think this is the nicest implementation I came up with. It doesn't quite have that magic dust that FastAPI has, but maybe someone with a better understanding of how to evaluate type annotations at runtime can take it to the next level.

from typing import Annotated, Any

from fastapi import FastAPI, Form, Depends
from pydantic import TypeAdapter

app = FastAPI()


def FormFromJson(alias:str, annotation: Any) -> Any:
    """Receive a JSON string from the Form in field 'alias',
    and convert it to type specified in 'annotation'"""
    adapt = TypeAdapter(annotation)
    def dependency_to_inject(
        # FastAPI only sees the JSON string
        value: Annotated[str, Form(alias=alias)]
    ):
        # But the dependency function knows how to change it to
        # the actual type the backend function wants
        return adapt.validate_json(value)
    return Depends(dependency_to_inject)
        

@app.post("/broken")
async def broken(
    json_list: Annotated[list[str], FormFromJson("json_list", list[str])]
) -> list[str]:
    return json_list