I have a use case where I want to create some values inside my class on the construction of the model. However, when I return the class to FastAPI for converting to JSON when I call the API, the constructor gets run again and I can get differing values from my original instance.
Here is a contrived example to illustrate:
class SomeModel(BaseModel):
public_value: str
secret_value: Optional[str]
def __init__(self, **data):
super().__init__(**data)
# this could also be done with default_factory
self.secret_value = randint(1, 5)
def some_function() -> SomeModel:
something = SomeModel(public_value="hello")
print(something)
return something
@app.get("/test", response_model=SomeModel)
async def exec_test():
something = some_function()
print(something)
return something
The console output is:
public_value='hello' secret_value=1
public_value='hello' secret_value=1
But the JSON in the web API is:
{
"public_value": "hello",
"secret_value": 2
}
When I step through the code I can see the __init__
being called twice.
First on the construction something = SomeModel(public_value="hello")
.
Second, which is unexpected to me, is in the API handler exec_test
on the return something
call.
If this is the wrong method to set up some internal data within the class please let me know the correct method to use. Otherwise, this seems like this is unintended behavior of one of the modules.
This is supposedly expected behavior when you use response_model
. It is not quite explained clearly in the docs, but in the Response Model section, it says that:
FastAPI will use this
response_model
to:
- Convert the output data to its type declaration.
- Validate the data.
...
When you return something
at the end of your route function exec_test
, FastAPI will convert that to another SomeModel
instance, do validation, and then return that validated instance. So, you get a different instance than the one you originally returned.
I've had this same issue before: Why does the response_model seem to __init__
the same object twice?, which led to this old issue: [BUG] Double validation pydantic model when used response_model. Most of the responses are along the lines of "it's expected":
This is something expected and the point of the
ResponseModel
, it ensures your data is in the correct order.
It's not exactly duplicated. Internally it validates once (if you add a
response_model
), but on your endpoint function you're manually validating once again.I believe the answer would be: it's the expected result. 😗
The solution I've since learned is to never mutate the models on __init__
like that. The answer provided by alex_noname using Field
or validators are, I also think, the best approaches to avoid this issue.
Here are the other workarounds if you really need that mutation on __init__
:
Just skip validation on the route function
In some_function
, where you are instantiating SomeModel
, that will already raise validation errors if the arguments are wrong (ex. public_value={'a': 1})
. Repeating the validation with response_model
could be redundant. There was even a PR on FastAPI to skip validation on response_model
but that never got merged.
You can just remove response_model
, and replace it with responses
to maintain the documentation with OpenAPI.
# @app.get("/test", response_model=SomeModel)
@app.get("/test", responses={200: {"model": SomeModel}})
async def exec_test():
something = some_function()
print(something)
return something
Let some_function
return the raw values of your model, rather than an instance of the model itself. Basically, moving/delaying the instantation and validation of your model onto the route function.
def some_function() -> dict:
# something = SomeModel(public_value="hello")
something = {"public_value": "hello"}
print(something)
return something
@app.get("/test", response_model=SomeModel)
async def exec_test():
something = some_function()
print(something)
return something
As the docs says, FastAPI will convert the return value to the response_model
type, thereby instantiating the model. Doing it here means validation will happen very late. Also, you'll have to deal with losing the convenience of using Pydantic models everywhere.
Related to #2, have 2 separate models, 1 for internal use and 1 for putting into the response_model
. This is similar to having separate input and output models example from the FastAPI docs:
class InternalModel(BaseModel):
public_value: str
class OutputModel(BaseModel):
public_value: str
secret_value: Optional[str]
def __init__(self, **data):
super().__init__(**data)
self.secret_value = randint(1, 5)
def some_function() -> InternalModel:
something = InternalModel(public_value="hello")
print(something)
return something
@app.get("/test", response_model=OutputModel)
async def exec_test():
something = some_function()
print(something)
return something
Since you are using response_model
for path operation, your return value is validated against it. But since you are returning an already validated instance of the model, this happens twice. It wouldn't be noticeable if you didn't use a mutating __init__
method that can generate different values for every instantiation of the model regardless of the input values.
I think the most preferable solution is to use the default_factory
function, since in this case the dynamic values ​​will be generated only at the time of instantiation of objects in your code, and the ready-made value will be used in the process of model validation while returning.
class SomeModel(BaseModel):
public_value: str
secret_value: int = Field(default_factory=lambda: randint(1, 5))
If for some reason you do not want to use the approach above, then you can use an always validator, that will allow you to check that the value is passed or it needs to be created:
class SomeModel(BaseModel):
public_value: str
secret_value: int = None
@validator('secret_value', pre=True, always=True)
def secret_validator(cls, v):
return randint(1, 5) if v is None else v
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With