Skip to content

Instantly share code, notes, and snippets.

@TomFaulkner
Last active May 11, 2023 09:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TomFaulkner/eaa6a379df2f8bb7e51d8ea3109ce8db to your computer and use it in GitHub Desktop.
Save TomFaulkner/eaa6a379df2f8bb7e51d8ea3109ce8db to your computer and use it in GitHub Desktop.
Remove fields from Pydantic models for FastAPI inputs

FastAPI and Pydantic are great, but in a situation where creating models through inheritence/mixins isn't practical (code generation from database queries, for example), it is difficult to remove keys from input models.

Having not found a good solution, here are two I've come up with.

Dynamically remove fields using create_model

I really wanted sans_fields to work, as it was my first solution and can be used dynamicly. Unfortunately due to typehint Forward Referencing it breaks if non-basic types are used (datetime, date, UUID, 'Foo', etc.). Calling update_forward_refs doesn't seem to fix things, neither does having the import in the namespace before running it. If objects are simple it works.

Optional field with schema_extra mixin

The second method I've found, that I expect I'll use going forward is a mixin that hides the field from FastAPI and allows makes the field optional. The downside is that it isn't dynamic. I think it is useful for foreign keys, indices, or creation date fields that you don't want to receive from the user. For this I created a mixin that makes the field optional and uses the schema_extra from Pydantic. The only other thing worth noting here is that the inheritance order matters, the mixin needs to go before the model.

If anyone finds a better way of doing this I would appreciate the comments.

Sources:

Removing from schema outputs: Though I think the documentation might have a mistake, I used delete rather than looping and popping.

https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization

from uuid import UUID
from typing import Sequence, Any
from pydantic import BaseModel, create_model
import datetime
def sans_fields(
name: str, model: BaseModel, filtered_fields=Sequence[str]
) -> BaseModel:
"""From a Pydantic model this will create a new model without the filtered_fields
Unfortunately, any typehints (datetime/date) that aren't base Python types will break things.
# pydantic.errors.ConfigError: field "a" not yet prepared so type is still a ForwardRef, you might need to call Baz.update_forward_refs().
I don't know of a way to resolve this at this time.
"""
return create_model(
name,
**{
k: (v, ...)
for k, v in model.__annotations__.items()
if k not in filtered_fields
}
)
# this seems to be a better way of doing things, though it isn't dynamic
class HidePrivateField(BaseModel):
private_field: UUID | None
class Config:
@staticmethod
def schema_extra(schema: dict[str, Any], model) -> None:
del schema['properties']['private_field']
# Usage
class RecordAdd_(pydantic.BaseModel):
private_field: int
other_field: str
class RecordAdd(pyd.HideCustomer, RecordAdd_): # inheritance order matters
pass
router = fastapi.APIRouter()
@router.post('/record', response_model=RecordAdd_)
def post(input: RecordAdd, private_field_number=fastapi.Depends(private_field_creator):
input.private_field = private_field_number
return do_a_thing(input)
@router.post('/record_two', response_model=RecordAdd_)
def post(
input: sans_fields("RecordAdd", RecordAdd,("private_field",)),
private_field_number=fastapi.Depends(private_field_creator
):
input.private_field = private_field_number
return do_a_thing(input)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment