Handling nested models in FastAPI form models

May 31, 2025

I have an API that looks something like this:

from fastapi import File, Form
from pydantic import BaseModel
class CreateMyObject(BaseModel):
name: str
file: File
@app.post("/my_object")
async def create_my_object(data: Annotated[CreateMyObject, Form()]) -> MyObject: ...

FastAPI has this cool concept of Form Models that allows you to treat a FormData input like a pydantic BaseModel. So, the API caller would send a multipart/form-data object.

formData = new FormData();
formData.append("name", "my_name");
formData.append("file", myFile, "my_filename.png");

On the backend, I can treat it as though it’s a JSON payload that FastAPI deserialized into a pydantic model for me. Nifty feature!

I recently had to extend this API to accept an arbitrary JSON payload:

from fastapi import File, Form
from pydantic import BaseModel
class CreateMyObject(BaseModel):
name: str
file: File
my_complicated_field: ComplicatedModel
@app.post("/my_object")
async def create_my_object(data: Annotated[CreateMyObject, Form()]) -> MyObject: ...

Here, ComplicatedModel is a heavily nested Pydantic object. This introduces an interesting problem: FormData only accepts primitive types—such as strings, blobs, or files—and, as you might imagine, ComplicatedModel is not primitive.

My naive approach was, client-side, to JSON.stringify() the object on my frontend, converting it to a string, to then pass to backend:

formData = new FormData();
formData.append("name", "my_name");
formData.append("file", myFile, "my_filename.png");
formData.append("my_complicated_field", JSON.stringify(myComplicatedObject));

Unfortunately, this causes a 422 Validation Error, as FastAPI expects my_complicated_field to be a nested object, and not a string. While we could annotate my_complicated_field as a str and manually perform Pydantic validation inside the model, I generally prefer to rely on API contracts. I really want to be able to statically tell my caller (as enforced via OpenAPI) that “I’m expecting this sort of payload, don’t waste your time with something else, thanks”!

Instead, I discovered you can use a field validator to achieve the desired solution.

from fastapi import File, Form
from pydantic import BaseModel, BeforeValidator
from pydantic_core import from_json
class CreateMyObject(BaseModel):
name: str
file: File
my_complicated_field: Annotated[
ComplicatedModel, BeforeValidator(lambda val: from_json(val))
]
@app.post("/my_object")
async def create_my_object(data: Annotated[CreateMyObject, Form()]) -> MyObject: ...

This solution is declarative with the BeforeValidator construct, requiring zero changes to any other code. I also learned that pydantic_core exports a from_json, which acts as a faster drop-in replacement for json.loads. I’ll take it!