fastapi
fastapi copied to clipboard
json_encoders from parent class is ignored in inherited pydantic models during serialization
Hi everyone !
We are currently using FastAPI to build a server on top of pydantic and it's really great, thanks for your work :slightly_smiling_face: . I encountered an issue when trying to serialize a model that inherits from another pydantic model. I have already checked and I don't think this is related to an issue in pydantic. I have a suggestion on how to solve it and I would be glad to do a PR for it. This issue is related to https://github.com/skalarsystems/fhirzeug/issues/50 .
Example
Here's a self-contained, minimal, reproducible, example with my use case:
import decimal
import pydantic
from fastapi.encoders import jsonable_encoder
class ModelWithEncoder(pydantic.BaseModel):
value: decimal.Decimal
class Config:
json_encoders = {decimal.Decimal: str}
class ChildModel(ModelWithEncoder):
class Config:
"""No json_encoders"""
value = decimal.Decimal("5.000")
parent_model = ModelWithEncoder(value=value)
child_model = ChildModel(value=value)
print(jsonable_encoder(parent_model))
# FastAPI uses encoder from pydantic
# {'value': '5.000'}
print(jsonable_encoder(child_model))
# Encoder from parent model is not used
# {'value': 5.0}
Description
- I want to serialize a pydantic model with a custom encoder, here for decimal values to be converted as strings instead of float (and therefore keep precision). The problem arose since I use inheritance to define the pydantic model.
- The expected behavior would be that both
parent_model
andchild_model
to be serialized the same way. - But in the child model, json encoder is ignored.
Environment
- Python version : 3.8.0
- FastAPI version : 0.55.1
Suggested solution
I think issue comes from this line. https://github.com/tiangolo/fastapi/blob/d60dd1b60e0acd0afcd5688e5759f450b6e7340c/fastapi/encoders.py#L53
The code is looking at the Config
attribute of the object which ignores all the parent ones. A 1-line solution to this problem would be to use the __config__
attribute instead which is well populated by pydantic :
encoder.update(getattr(obj.__config__, "json_encoders", {}))
If issue is confirmed, I can create the pull request with the workaround and a test.
I think this might be little confusing, I expecting that defining new config will override existing one, not just extend it as it done in Pydantic currently. Furthermore you can miss that you have some overrides in code with deep nesting and making this behaviour as default can also ruin existent code.
I think we can add extra flag to BaseModel/BaseConfig (or even to APIRoute but this is overkill imo) that allow us to toggle between obj.Config and obj.__config__logic.
This may help.
class ChildModel(ModelWithEncoder):
class Config(ModelWithEncoder.Config):
"""No json_encoders"""
I think the point is, the behavior of pydantic and pydantic in fastAPI ist different. This lead us to confusion since its not quite clear. If I extend the example here:
import decimal
import pydantic
from fastapi.encoders import jsonable_encoder
class ModelWithEncoder(pydantic.BaseModel):
value: decimal.Decimal
class Config:
json_encoders = {decimal.Decimal: str}
class ChildModel(ModelWithEncoder):
class Config:
"""No json_encoders"""
value = decimal.Decimal("5.000")
parent_model = ModelWithEncoder(value=value)
child_model = ChildModel(value=value)
print(jsonable_encoder(parent_model))
# FastAPI uses encoder from pydantic
# {'value': '5.000'}
print(jsonable_encoder(child_model))
# Encoder from parent model is not used
# {'value': 5.0}
print(child_model.json())
# This behaves differently :(
# {"value": "5.000"}
For me as a user it would be nice if behavior is consistent here.
(EDIT: spelling)