Skip to content

Commit cf30eb0

Browse files
zzstoatzzclaude
andcommitted
fix: handle pydantic generic models in JSON serializer
When using parameterized Pydantic generic models like `APIResult[str]`, the JSON serializer was storing class names with brackets (e.g., `module.APIResult[str]`) which cannot be imported by Python's import system, causing deserialization to fail. This fix extracts the origin class from Pydantic's generic metadata, ensuring the serialized class name is importable (e.g., `module.APIResult`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 89fe615 commit cf30eb0

File tree

2 files changed

+54
-2
lines changed

2 files changed

+54
-2
lines changed

src/prefect/serializers.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@
4141
_TYPE_ADAPTER_CACHE: dict[str, TypeAdapter[Any]] = {}
4242

4343

44+
def _get_importable_class(cls: type) -> type:
45+
"""
46+
Get an importable class from a potentially parameterized generic.
47+
48+
For Pydantic generic models like `APIResult[str]`, the class name includes
49+
type parameters (e.g., `APIResult[str]`) which cannot be imported. This
50+
function extracts the origin class (e.g., `APIResult`) which can be imported.
51+
"""
52+
if hasattr(cls, "__pydantic_generic_metadata__"):
53+
origin = cls.__pydantic_generic_metadata__.get("origin")
54+
if origin is not None:
55+
return origin
56+
return cls
57+
58+
4459
def prefect_json_object_encoder(obj: Any) -> Any:
4560
"""
4661
`JSONEncoder.default` for encoding objects into JSON with extended type support.
@@ -58,8 +73,9 @@ def prefect_json_object_encoder(obj: Any) -> Any:
5873
),
5974
}
6075
else:
76+
importable_class = _get_importable_class(obj.__class__)
6177
return {
62-
"__class__": to_qualified_name(obj.__class__),
78+
"__class__": to_qualified_name(importable_class),
6379
"data": custom_pydantic_encoder({}, obj),
6480
}
6581

tests/test_serializers.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import uuid
55
from dataclasses import dataclass
6-
from typing import Any
6+
from typing import Any, Generic, TypeVar
77
from unittest.mock import MagicMock
88

99
import pytest
@@ -30,6 +30,16 @@ class MyModel(BaseModel):
3030
y: uuid.UUID
3131

3232

33+
T = TypeVar("T")
34+
35+
36+
class GenericResult(BaseModel, Generic[T]):
37+
"""Generic model for testing JSON serialization of parameterized types."""
38+
39+
data: T | None = None
40+
message: str = ""
41+
42+
3343
@dataclass
3444
class MyDataclass:
3545
x: int
@@ -387,6 +397,32 @@ def test_does_not_allow_default_collision(self):
387397
with pytest.raises(ValidationError):
388398
JSONSerializer(dumps_kwargs={"default": "foo"})
389399

400+
def test_pydantic_generic_model_roundtrip(self):
401+
"""Test that Pydantic generic models with type parameters can be serialized.
402+
403+
Regression test for: https://github.com/PrefectHQ/prefect/issues/XXXX
404+
405+
When using parameterized generics like `APIResult[str]`, the class name
406+
includes brackets which cannot be imported. The serializer should extract
407+
the origin class for proper roundtrip serialization.
408+
"""
409+
serializer = JSONSerializer()
410+
411+
# Test with concrete type parameter
412+
result = GenericResult[str](data="hello", message="success")
413+
serialized = serializer.dumps(result)
414+
415+
# Verify the serialized class name doesn't include type parameters
416+
decoded = json.loads(serialized)
417+
assert "[" not in decoded["__class__"], (
418+
f"Class name should not contain brackets: {decoded['__class__']}"
419+
)
420+
421+
# Verify roundtrip works
422+
loaded = serializer.loads(serialized)
423+
assert loaded.data == "hello"
424+
assert loaded.message == "success"
425+
390426

391427
class TestCompressedSerializer:
392428
@pytest.mark.parametrize("data", SERIALIZER_TEST_CASES)

0 commit comments

Comments
 (0)