Partial update in FastAPI

Question:

I want to implement a put or patch request in FastAPI that supports partial update. The official documentation is really confusing and I can’t figure out how to do the request. (I don’t know that items is in the documentation since my data will be passed with request’s body, not a hard-coded dict).

class QuestionSchema(BaseModel):
    title: str = Field(..., min_length=3, max_length=50)
    answer_true: str = Field(..., min_length=3, max_length=50)
    answer_false: List[str] = Field(..., min_length=3, max_length=50)
    category_id: int


class QuestionDB(QuestionSchema):
    id: int


async def put(id: int, payload: QuestionSchema):
    query = (
        questions
        .update()
        .where(id == questions.c.id)
        .values(**payload)
        .returning(questions.c.id)
    )
    return await database.execute(query=query)

@router.put("/{id}/", response_model=QuestionDB)
async def update_question(payload: QuestionSchema, id: int = Path(..., gt=0),):
    question = await crud.get(id)
    if not question:
        raise HTTPException(status_code=404, detail="question not found")

    ## what should be the stored_item_data, as documentation?
    stored_item_model = QuestionSchema(**stored_item_data)
    update_data = payload.dict(exclude_unset=True)
    updated_item = stored_item_model.copy(update=update_data)

    response_object = {
        "id": question_id,
        "title": payload.title,
        "answer_true": payload.answer_true,
        "answer_false": payload.answer_false,
        "category_id": payload.category_id,
    }
    return response_object

How can I complete my code to get a successful partial update here?

Asked By: Saeed Esmaili

||

Answers:

I got this answer on the FastAPI’s Github issues.

You could make the fields Optional on the base class and create a new QuestionCreate model that extends the QuestionSchema. As an example:

from typing import Optional

class Question(BaseModel):
    title: Optional[str] = None  # title is optional on the base schema
    ...

class QuestionCreate(Question):
   title: str  # Now title is required

The cookiecutter template here provides some good insight too.

Answered By: Saeed Esmaili

Posting this here for googlers who are looking for an intuitive solution for creating Optional Versions of their pydantic Models without code duplication.

Let’s say we have a User model, and we would like to allow for PATCH requests to update the User. But we need to create a schema that tells FastApi what to expect in the content body, and specifically that all the fields are Optional (Since that’s the nature of PATCH requests). We can do so without redefining all the fields

from pydantic import BaseModel
from typing import Optional

# Creating our Base User Model
class UserBase(BaseModel):
   username: str
   email: str
   

# And a Model that will be used to create an User
class UserCreate(UserBase):
   password: str

Code Duplication ❌

class UserOptional(UserCreate):
    username: Optional[str]
    email: Optional[str]
    password: Optional[str]

One Liner ✅

# Now we can make a UserOptional class that will tell FastApi that all the fields are optional. 
# Doing it this way cuts down on the duplication of fields
class UserOptional(UserCreate):
    __annotations__ = {k: Optional[v] for k, v in UserCreate.__annotations__.items()}

NOTE: Even if one of the fields on the Model is already Optional, it won’t make a difference due to the nature of Optional being typing.Union[type passed to Optional, None] in the background.

i.e typing.Union[str, None] == typing.Optional[str]


You can even make it into a function if your going to be using it more than once:

def convert_to_optional(schema):
    return {k: Optional[v] for k, v in schema.__annotations__.items()}

class UserOptional(UserCreate):
    __annotations__ = convert_to_optional(UserCreate)

Answered By: cdraper

Based on the answer of @cdraper, I made a partial model factory:

from typing import Mapping, Any, List, Type
from pydantic import BaseModel

def model_annotations_with_parents(model: BaseModel) -> Mapping[str, Any]:
    parent_models: List[Type] = [
        parent_model for parent_model in model.__bases__
        if (
            issubclass(parent_model, BaseModel)
            and hasattr(parent_model, '__annotations__')
        )
    ]

    annotations: Mapping[str, Any] = {}

    for parent_model in reversed(parent_models):
        annotations.update(model_annotations_with_parents(parent_model))

    annotations.update(model.__annotations__)
    return annotations


def partial_model_factory(model: BaseModel, prefix: str = "Partial", name: str = None) -> BaseModel:
    if not name:
        name = f"{prefix}{model.__name__}"

    return type(
        name, (model,),
        dict(
            __module__=model.__module__,
            __annotations__={
                k: Optional[v]
                for k, v in model_annotations_with_parents(model).items()
            }
        )
    )


def partial_model(cls: BaseModel) -> BaseModel:
    return partial_model_factory(cls, name=cls.__name__)

Can be used with the function partial_model_factory:

PartialQuestionSchema = partial_model_factory(QuestionSchema)

Or with decorator partial_model:

@partial_model
class PartialQuestionSchema(QuestionSchema):
    pass
Answered By: Felipe Buccioni

I created a library (pydantic-partial) just for that, converting all the fields in the normal DTO model to being optional. See https://medium.com/@david.danier/how-to-handle-patch-requests-with-fastapi-c9a47ac51f04 for a code example and more detailed explanation.

https://github.com/team23/pydantic-partial/

Answered By: David Danier