fastapi icon indicating copy to clipboard operation
fastapi copied to clipboard

json_encoders from parent class is ignored in inherited pydantic models during serialization

Open Wauplin opened this issue 4 years ago • 3 comments

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 and child_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.

Wauplin avatar Jul 16 '20 13:07 Wauplin

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.

SirTelemak avatar Jul 16 '20 21:07 SirTelemak

This may help.

class ChildModel(ModelWithEncoder):
    class Config(ModelWithEncoder.Config):
        """No json_encoders"""

linchiwei123 avatar Jul 17 '20 15:07 linchiwei123

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)

julian-r avatar Jul 21 '20 14:07 julian-r