4

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?

Community
  • 1
  • 1
Saeed Esmaili
  • 764
  • 3
  • 12
  • 34
  • stored_item_data is the data that you get into your question variable. Basically, if your question is a dict with the old values, substitute the old values with the new ones (payload variable) and replace the entire row with the combined values (old and new) in the database. The docs shows a general case, you should implement the update on the database yourself, not fastapi – lsabi May 13 '20 at 20:58

4 Answers4

6

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.

Paolo
  • 20,112
  • 21
  • 72
  • 113
Saeed Esmaili
  • 764
  • 3
  • 12
  • 34
5

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)

cdraper
  • 147
  • 2
  • 7
  • The disadvantage is that this approach is not friendly to linters (e.g. mypy) or IDEs. Also for mypy the base class should have optional values. – Paweł Nadolski Apr 13 '23 at 10:29
2

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
Felipe Buccioni
  • 19,109
  • 2
  • 28
  • 28
1

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/

  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/32636793) – mkrieger1 Sep 08 '22 at 09:43