nayaverdier / dyntastic

A DynamoDB library on top of Pydantic and boto3.
MIT License
57 stars 14 forks source link

serialization does not honor json_encoders from Pydantic Model #8

Closed flolas closed 11 months ago

flolas commented 1 year ago

given the following code:

class DummyJobClass:
     pass

class JobType(Enum):
    DUMMY_JOB_TYPE = DummyJobClass

class Job(Dyntastic):
    __table_name__ = "jobs"
    __hash_key__ = "job_id"
    __table_host__ = "http://localhost:8000"
    __table_region__ = "us-east-1"

    job_id: int
    type: JobType

    class Config:
        json_encoders = {
           JobType: lambda obj: obj.name
        }

model = Job(job_id=1, type=JobType.DUMMY_JOB_TYPE)
model.save()

Error:

File "pydantic/json.py", line 90, in pydantic.json.pydantic_encoder
TypeError: Object of type 'type' is not JSON serializable

This happens because the serialize function uses the pydantic_encoder which works for dataclasses because does not implement .json() method.

https://github.com/nayaverdier/dyntastic/blob/4b90aaa5d8dec7013de4c9692da44a7aa2766b0d/dyntastic/attr.py#L17C1-L33C70

For Sets and Decimals, we can implement a json_encoder in Dyntastic, but if someone overrides Config class does not work 😢.

Another option would be implement something like this: https://github.com/pydantic/pydantic/blob/d9c2af3a701ca982945a590de1a1da98b3fb4003/pydantic/main.py#L242-L245

in this part of serialize:

    else:
        # handle types like datetime
        return json.loads(json.dumps(data, default=pydantic_encoder))
flolas commented 1 year ago

workaround:


class CustomSerializerDyntasticMixin:
    @staticmethod
    def serialize(data, model_json_encoders=None):
        _serialize = partial(CustomSerializerDyntasticMixin.serialize, model_json_encoders=model_json_encoders)
        if isinstance(data, BaseModel):
            return _serialize(data.dict())
        elif isinstance(data, dict):
            return {key: _serialize(value) for key, value in data.items() if value is not None}
        elif isinstance(data, (list, tuple)):
            return list(map(_serialize, data))
        elif isinstance(data, set):
            return set(map(_serialize, data))
        elif isinstance(data, (Decimal, str, int, bytes, bool, float, type(None))):
            return data
        else:
            if model_json_encoders:
                json_encoder = partial(custom_pydantic_encoder, model_json_encoders)
            else:
                json_encoder = pydantic_encoder
            return json.loads(json.dumps(data, default=json_encoder))

    def save(self, *, condition: Optional[object] = None):
        data = self.dict(by_alias=True)
        dynamo_serialized = CustomSerializerDyntasticMixin.serialize(data, model_json_encoders=self.__config__.json_encoders)
        return self._dyntastic_call("put_item", Item=dynamo_serialized, ConditionExpression=condition)

class DummyJobClass:
     pass

class JobType(Enum):
    DUMMY_JOB_TYPE = DummyJobClass

class Job(Dyntastic):
    __table_name__ = "jobs"
    __hash_key__ = "job_id"
    __table_host__ = "http://localhost:8000"
    __table_region__ = "us-east-1"

    job_id: int
    type: JobType

    class Config:
        json_encoders = {
           JobType: lambda obj: obj.name
        }

model = Job(job_id=1, type=JobType.DUMMY_JOB_TYPE)
model.save()

I can make a PR if you would like

nayaverdier commented 1 year ago

As a different workaround, does something like this work?

from pydantic.json import ENCODERS_BY_TYPE
ENCODERS_BY_TYPE[JobType] = lambda obj: obj.name
flolas commented 1 year ago

@nayaverdier I ended up with a custom Enum subclass to handle Enums with an object value because Pydantic v1 has issues with Enums. These issues are fixed in Pydantic v2.

nayaverdier commented 1 year ago

Pydantic v2 support is on my roadmap, I'll let you know when it's released.

nayaverdier commented 11 months ago

@flolas Dyntastic version 0.13.0 now supports pydantic v2. Closing this ticket but feel free to reopen if you continue to have issues related to this.