Issue
I'm making an API with FastAPI and Pydantic.
I would like to have some PATCH endpoints, where 1 or N fields of a record could be edited at once. Moreover, I would like the client to only pass the necessary fields in the payload.
Example:
class Item(BaseModel):
name: str
description: str
price: float
tax: float
@app.post("/items", response_model=Item)
async def post_item(item: Item):
...
@app.patch("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
...
In this example, for the POST request, I want every field to be required. However, in the PATCH endpoint, I don't mind if the payload only contains, for example, the description field. That's why I wish to have all fields as optional.
Naive approach:
class UpdateItem(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
tax: Optional[float]
But that would be terrible in terms of code repetition.
Any better option?
Solution
Solution with metaclasses
I've just come up with the following:
class AllOptional(pydantic.main.ModelMetaclass):
def __new__(self, name, bases, namespaces, **kwargs):
annotations = namespaces.get('__annotations__', {})
for base in bases:
annotations.update(base.__annotations__)
for field in annotations:
if not field.startswith('__'):
annotations[field] = Optional[annotations[field]]
namespaces['__annotations__'] = annotations
return super().__new__(self, name, bases, namespaces, **kwargs)
Use it as:
class UpdatedItem(Item, metaclass=AllOptional):
pass
So basically it replace all non optional fields with Optional
Any edits are welcome!
With your example:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
import pydantic
app = FastAPI()
class Item(BaseModel):
name: str
description: str
price: float
tax: float
class AllOptional(pydantic.main.ModelMetaclass):
def __new__(self, name, bases, namespaces, **kwargs):
annotations = namespaces.get('__annotations__', {})
print(bases)
print(bases[0].__annotations__)
for base in bases:
annotations = {**annotations, **base.__annotations__}
for field in annotations:
if not field.startswith('__'):
annotations[field] = Optional[annotations[field]]
namespaces['__annotations__'] = annotations
return super().__new__(self, name, bases, namespaces, **kwargs)
class UpdatedItem(Item, metaclass=AllOptional):
pass
# This continues to work correctly
@app.get("/items/{item_id}", response_model=Item)
async def get_item(item_id: int):
return {
'name': 'Uzbek Palov',
'description': 'Palov is my traditional meal',
'price': 15.0,
'tax': 0.5,
}
@app.patch("/items/{item_id}") # not using response_model=Item
async def update_item(item_id: str, item: UpdatedItem):
return item
Answered By - Drdilyor
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.