Let's say I have a Pydantic model with validation:
Name = Annotated[str, AfterValidator(validate_name)]
class Foo(BaseModel):
id: UUID = Field(default_factory=uuid4)
name: Name
And a FastAPI endpoint:
@app.post('/foos')
def create_foo(foo: Foo) -> Foo:
save_to_database(foo)
return foo
I only want the caller to be able to pass a value for name
, but not for id
. Is there any way to do something like this?
def create_foo(foo: Annotated[Foo, Body(include=['id'])]) -> Foo:
I know I can do:
@app.post('/foos')
def create_foo(name: Annotated[str, Body(embed=True)]) -> Foo:
foo = Foo(name=name)
save_to_database(foo)
return foo
But then the implicit validation error handling doesn't work anymore, and I need to add more code to do that.
Any elegant way of handling that?
You could hide/exclude a field on object instantiation/creation, by using Private model attributes. Pydantic will exclude model attributes that have a leading underscore. As described in the linked documentation:
Attributes whose name has a leading underscore are not treated as fields by Pydantic, and are not included in the model schema. Instead, these are converted into a "private attribute" which is not validated or even set during calls to
__init__
,model_validate
, etc.
Note, though, that:
As of Pydantic v2.1.0, you will receive a
NameError
if trying to use theField
function with a private attribute. Because private attributes are not treated as fields (as mentioned earlier), theField()
function cannot be applied.
Thus, in Pydantic V2, you could use the PrivateAttr
instead of Field
function, along with the default_factory
parameter, in order to define a callable that will be called to generate a dynamic default value (i.e., different for each model instance)—in this case, a UUID
.
from fastapi import FastAPI
from pydantic import BaseModel, PrivateAttr
from uuid import UUID, uuid4
class Foo(BaseModel):
_id: UUID = PrivateAttr(default_factory=uuid4)
name: str
app = FastAPI()
@app.post("/foo")
def create_foo(foo: Foo):
print(foo._id)
return foo
Simply a variation of the above (see the documentation for more details), without using the PrivateAttr
and default_factory
. Instead, the __init__
method is directly used to automatically generate a new UUID.
from fastapi import FastAPI
from pydantic import BaseModel
from uuid import UUID, uuid4
class Foo(BaseModel):
_id: UUID
name: str
def __init__(self, **data):
super().__init__(**data)
self._id = uuid4()
app = FastAPI()
@app.post("/foo")
def create_foo(foo: Foo):
print(foo._id)
return foo
Another way would be to use two different Pydantic models, one meant to be used by the user, while the second one, which should inherit from the first (base) one, by the backend. Similar examples are given in FastAPI's Extra Models documentation, as well as in Full Stack FastAPI Template.
from fastapi import FastAPI
from pydantic import BaseModel, Field
from uuid import UUID, uuid4
class BaseFoo(BaseModel):
name: str
class Foo(BaseFoo):
id: UUID = Field(default_factory=uuid4)
app = FastAPI()
@app.post("/foo")
def create_foo(base: BaseFoo):
foo = Foo(**base.model_dump()) # Foo(name=base.name) should work as well
print(foo.id)
return base
This is a variation of Options 1 and 2, modified in a way that would allow one to define their private attribute without using an underscore (if that's a requirement in your project), but still get the same result as if any of the previous options were used.
In this case, the Field
function is used. Since the attribute is meant to be hidden from the client, you should need to set the exclude
attribute to True
, so that if you return the Pydantic model instance back to the client, that attribute won't be included. Also, in Pydantic V2, you could use the SkipJsonSchema
annotation, in order to skip that field from the generated JSON schema, as in Swagger UI autodocs, for instance (for Pydantic V1 solutions, please have a look at this github post and its related discussion).
Now, there is nothing from stopping the client to pass the hidden attribute in their JSON request body, regardless of hiding it from the schema and defining it as optional/non-required (i.e., Field(default=None)
). Since this solution uses a regular Field
and not PrivateAttr
, using the default_factory
attribute, as in Option 1 and as shown below:
class Foo(BaseModel):
id: SkipJsonSchema[UUID] = Field(default_factory=uuid4, exclude=True)
name: str
would not be the most suitalbe approach, as if the client passed a value for id
, that value would be assigned to that field.
However, using a similar approach to Option 2, i.e., replacing default_factory
with __init__
(which is used to generate the UUID
for id
field), even if the client passed a value for id
, it would be "ignored", and the one generated by the model would be assigned to the field.
from fastapi import FastAPI
from pydantic import BaseModel, Field
from uuid import UUID, uuid4
from pydantic.json_schema import SkipJsonSchema
class Foo(BaseModel):
id: SkipJsonSchema[UUID] = Field(default=None, exclude=True)
name: str
def __init__(self, **data):
super().__init__(**data)
self.id = uuid4()
app = FastAPI()
@app.post("/foo")
def create_foo(foo: Foo):
print(foo.id)
return foo
This answer and this answer might also prove helpful to future readers.
default_factory
or __init__
overwriting the provided field valueWhile this is not an issue when using Option 3 provided above (and one could opt going for that option, if they wish), it might be when using one of the remaining options, depending on the method used to populate the model.
For instance, if you populate the model using Foo(**data)
, where data
is an already existing model instance from your database, using one of the options described earlier (besides Option 3 that does not suffer from this issue), the _id
or id
value (included in the data
dictionary) that is passed to the model would be replaced/overwritten by a newly generated one.
To overcome this issue, the following solutions are suggested.
After calling Foo(**data)
, you could simply replace the newly generated id with the existing one, by setting the value for that specific field to data["_id"]
or data["id"]
(depending on the Option used).
Example:
data = {"name": "foo", "_id": "7c4308a2-0f32-1243-b2ad-bf214a24a5aa"}
f = Foo(**data)
f._id = data["_id"]
Instead of using Foo(**data)
, which uses the model's __init__
method, and hence, a new id value is generated when called—for the sake of completeness, it should also be noted that there is an additional method, i.e., model_validate()
, which is very similar to the __init__
method of the model, except it takes a dict
or an object rather than keyword arguments—one could use the model_construct()
method (in Pydantic V1, this used to be construct()
).
As per the documentation:
Creating models without validation
Pydantic also provides the
model_construct()
method, which allows models to be created without validation. This can be useful in at least a few cases:
- when working with complex data that is already known to be valid (for performance reasons)
- when one or more of the validator functions are non-idempotent, or
- when one or more of the validator functions have side effects that you don't want to be triggered.
Warning
model_construct()
does not do any validation, meaning it can create models which are invalid. You should only ever use themodel_construct()
method with data which has already been validated, or that you definitely trust.[...]
When constructing an instance using
model_construct()
, no__init__
method from the model or any of its parent classes will be called, even when a custom__init__
method is defined.
Example:
data = {"name": "foo", "_id": "7c4308a2-0f32-1243-b2ad-bf214a24a5aa"}
f = Foo.model_construct(**data)