pydantic icon indicating copy to clipboard operation
pydantic copied to clipboard

model_serializer doesn't return dataclasses as custom type

Open bedlamzd opened this issue 6 months ago • 6 comments

Initial Checks

  • [X] I confirm that I'm using Pydantic V2

Description

I've been exploring the idea to parse pydantic models into a ORM object using custom serializer. For testing I've been using dataclassses as ORM classes and discovered that they are serialized to JSON even when returned explicitly in model serializer. Same happens with pydantic objects.

But when using plain python classes, I get the expected behavior - serialization returns instances of custom "ORM" classes.

It'd be nice if documentation mentioned that pydantic classes and dataclasses can't be returned as-is when serializing.

Example Code

from dataclasses import dataclass
from typing import Any, ClassVar, cast

from pydantic import BaseModel, SerializationInfo, SerializerFunctionWrapHandler, model_serializer


class CustomBase(BaseModel):
    orm_model: ClassVar

    @model_serializer(mode="wrap")
    def wrap_orm_serialize(self, handler: SerializerFunctionWrapHandler, info: SerializationInfo):
        serialized = handler(self)
        context = cast(dict[str, Any], info.context) if info.context else {}
        if context.get("to_orm"):
            ormed = self.orm_model(**serialized)
            return ormed
        return serialized


@dataclass
class DataclassTestOneOrm:
    one: str


@dataclass
class DataclassTestTwoOrm:
    two: int
    to: DataclassTestOneOrm


class TestOne(CustomBase):
    orm_model = DataclassTestOneOrm

    one: str


class TestTwo(CustomBase):
    orm_model = DataclassTestTwoOrm

    two: int
    to: TestOne


to = TestOne(one="ONE")
tt = TestTwo(two=222, to=to)

dtt = tt.model_dump()
print(f"{dtt=}")  # dtt={'two': 222, 'to': {'one': 'ONE'}}

dtt = tt.model_dump(context={"to_orm": True})

print(f"{dtt=}")  # dtt={'two': 222, 'to': {'one': 'ONE'}}

# Though the following works as expected

class PlainTestOneOrm:
    one: str

    def __init__(self, one) -> None:
        self.one = one

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(one={self.one})"


class PlainTestTwoOrm:
    two: int
    to: PlainTestOneOrm

    def __init__(self, two, to) -> None:
        self.two = two
        self.to = to

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(two={self.two}, to={self.to})"


class TestOne(CustomBase):
    orm_model = PlainTestOneOrm

    one: str


class TestTwo(CustomBase):
    orm_model = PlainTestTwoOrm

    two: int
    to: TestOne


to = TestOne(one="ONE")
tt = TestTwo(two=222, to=to)

dtt = tt.model_dump()  # dtt={'two': 222, 'to': {'one': 'ONE'}}
print(f"{dtt=}")

dtt = tt.model_dump(context={"to_orm": True})

print(f"{dtt=}")  # dtt=PlainTestTwoOrm(two=222, to=PlainTestOneOrm(one=ONE))

Python, Pydantic & OS Version

pydantic version: 2.7.1
        pydantic-core version: 2.18.2
          pydantic-core build: profile=release pgo=true
                 install path: /home/maksim_borisov/repos/hogwarts/api-scratchpad/.venv/lib/python3.12/site-packages/pydantic
               python version: 3.12.4 (main, Jun  7 2024, 00:00:00) [GCC 14.1.1 20240607 (Red Hat 14.1.1-5)]
                     platform: Linux-5.15.153.1-microsoft-standard-WSL2-x86_64-with-glibc2.39
             related packages: pyright-1.1.376 fastapi-0.111.0 typing_extensions-4.12.2 pydantic-settings-2.2.1 pyright-1.1.376 fastapi-0.111.0 typing_extensions-4.12.2 pydantic-settings-2.2.1
                       commit: unknown

bedlamzd avatar Aug 19 '24 01:08 bedlamzd