PacktPublishing / Building-Data-Science-Applications-with-FastAPI

Building Data Science Applications with FastAPI, Published by Packt
MIT License
299 stars 152 forks source link

Chapter 4 `PostPartialUpdate` model errors after meeting `response_model=PostPublic` #6

Closed gitgithan closed 1 year ago

gitgithan commented 1 year ago

Edit

Below is no longer an issue. My confusion arose because I set exclude_unset=False to investigate its effects, then gave only {"title": "string"} when calling the endpoint. This combination leads to Internal Server Error which prompted this issue. If we do it properly by setting exclude_unset=True, there are no issues with the question below

To allow user to call update endpoint with only title, with only content, and with none of both?

Nevertheless the investigations on effects of exclude_unset=False could be helpful to other readers. There are 2 independent dimensions of experimentation

  1. exclude_unset True/False
  2. Include both title and content when calling, or have one of/both missing

This post is half error reporting and half what I wish was explained better.

I was testing chapter4_working_pydantic_objects_05.py. First I edited the file to set up dummy data because it was empty and cannot be tested (will always get 404)

class DummyDatabase:
    posts: Dict[int, PostDB] = {1:PostDB(id=1,title='title1',content='content1',nb_views=10)}

Then from automated docs, I called the endpoint with {"title": "string"}, intentionally leaving out content key that was suggested by the docs (or else cannot see effect of exclude_unset=True).

Then terminal errors with

/site-packages/fastapi/routing.py", line 138, in serialize_response
    raise ValidationError(errors, field.type_)
pydantic.error_wrappers.ValidationError: 1 validation error for PostPublic
response -> content

I guess this is because PostPublic which was inherited from PostBase which had content:str type which did not allow None.

Question: So how do we make this work? (To allow user to call update endpoint with only title, with only content, and with none of both?). Making fields in PostBase optional doesn't seem like the correct way to handle this.

On a separate but related issue, this lesson could have been clearer on how a user could understand the effects of exclude_unset=True. To look at what this parameter is doing, and to solve the above issue, I

  1. removed response_model=PostPublic from the path decorator so it doesn't constrain response types (which solves this issue, probably wrongly though)
  2. Added print(updated_fields) to the code
  3. called the update endpoint with {"title": "string"} (you won't see the effect of exclude_unset if you used default {"title": "string", "content": "string"} suggested by automated docs)

Response Body:

Explanation When exclude_unset=False, the update endpoint was not given content key when called. PostPartialUpdate did not exclude what was unset/not given (content), so it filled in content with None from content: Optional[str] = None. This content:None was then put into the response through post_db.copy(update=updated_fields). When exclude_unset=True, the update endpoint was also not given the content key when called. PostPartialUpdate excluded what was not unset/not given (content), so updated_fields contains only the title, leaving content as the original hardcoded value

frankie567 commented 1 year ago

I'm not sure what your question is here but to be more clear about exclude_unset: it's a way to tell Pydantic to give us only the fields that were actually given in the request and discard the others.

So if we have a model like this:

class PostPartialUpdate(BaseModel):
    title: Optional[str] = None
    content: Optional[str] = None

And we make this request:

{"title": "hello"}

Here is what we get without and with exclude_unset:

updated_fields = post_update.dict()
print(updated_fields)  # {"title": "hello", "content": None}

updated_fields = post_update.dict(exclude_unset=True)
print(updated_fields)  # {"title": "hello"}

With exclude_unset, only title is output in the dictionary, not content.

This is useful for partial updates, when we want to update only a subset of fields without having to repeat the fields that are not changing.

It's important because when we're doing this:

updated_post = post_db.copy(update=updated_fields)

We override every fields set in updated_fields.

In your experiment with exclude_unset=False, updated_fields was {"title": "hello", "content": None}. Hence, we also set content to None. This is not valid for the PostPublic, because content is required and can't be None, hence the error you were getting.

gitgithan commented 1 year ago

Thanks for this example. I was initially doubting the code provided was able to handle incomplete inputs (not having both the 2 keys mentioned in PostPartialUpdate) like

  1. {}
  2. {'title': 'string'}
  3. {'content': 'string'}

because i received internal server error. Later i realized that was because i set exclude_unset=False when testing incomplete inputs. If i had left exclude_unset=True as given, there's nothing wrong with the code in handling incomplete inputs.

frankie567 commented 1 year ago

Yes, with exclude_unset=False, that's totally expected :)

Cheers!