Skip to content

Commit 7cd7166

Browse files
committed
feat: add GeneralSerde for unified JSON serialization/deserialization
- Introduce GeneralSerde to handle both Pydantic BaseModel instances and regular JSON objects. - Use model_dump_json for Pydantic models, falling back to json.dumps for non-Pydantic objects. - Maintain consistency with existing serde implementations.
1 parent 66e6c68 commit 7cd7166

File tree

5 files changed

+83
-34
lines changed

5 files changed

+83
-34
lines changed

python/restate/handler.py

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717

1818
from dataclasses import dataclass
1919
from inspect import Signature
20-
from typing import Any, Callable, Awaitable, Generic, Literal, Optional, TypeVar
20+
from typing import Any, Awaitable, Callable, Generic, Literal, Optional, TypeVar
2121

2222
from restate.exceptions import TerminalError
23-
from restate.serde import JsonSerde, Serde, PydanticJsonSerde
23+
from restate.serde import GeneralSerde, PydanticBaseModel, PydanticJsonSerde, Serde
2424

2525
I = TypeVar('I')
2626
O = TypeVar('O')
@@ -29,22 +29,6 @@
2929
# we will use this symbol to store the handler in the function
3030
RESTATE_UNIQUE_HANDLER_SYMBOL = str(object())
3131

32-
33-
def try_import_pydantic_base_model():
34-
"""
35-
Try to import PydanticBaseModel from Pydantic.
36-
"""
37-
try:
38-
from pydantic import BaseModel # type: ignore # pylint: disable=import-outside-toplevel
39-
return BaseModel
40-
except ImportError:
41-
class Dummy: # pylint: disable=too-few-public-methods
42-
"""a dummy class to use when Pydantic is not available"""
43-
44-
return Dummy
45-
46-
PydanticBaseModel = try_import_pydantic_base_model()
47-
4832
@dataclass
4933
class ServiceTag:
5034
"""
@@ -103,15 +87,15 @@ def extract_io_type_hints(handler_io: HandlerIO[I, O], signature: Signature):
10387

10488
if is_pydantic(annotation):
10589
handler_io.input_type.is_pydantic = True
106-
if isinstance(handler_io.input_serde, JsonSerde): # type: ignore
90+
if isinstance(handler_io.input_serde, GeneralSerde): # type: ignore
10791
handler_io.input_serde = PydanticJsonSerde(annotation)
10892

10993
annotation = signature.return_annotation
11094
handler_io.output_type = TypeHint(annotation=annotation, is_pydantic=False)
11195

11296
if is_pydantic(annotation):
11397
handler_io.output_type.is_pydantic=True
114-
if isinstance(handler_io.output_serde, JsonSerde): # type: ignore
98+
if isinstance(handler_io.output_serde, GeneralSerde): # type: ignore
11599
handler_io.output_serde = PydanticJsonSerde(annotation)
116100

117101
@dataclass
@@ -138,6 +122,11 @@ def make_handler(service_tag: ServiceTag,
138122
signature: Signature) -> Handler[I, O]:
139123
"""
140124
Factory function to create a handler.
125+
126+
Note:
127+
This function mutates the `handler_io` parameter by updating its type hints
128+
and serdes based on the function signature. Callers should be aware that the
129+
passed `handler_io` instance will be modified.
141130
"""
142131
# try to deduce the handler name
143132
handler_name = name
@@ -150,7 +139,7 @@ def make_handler(service_tag: ServiceTag,
150139
raise ValueError("Handler must have at least one parameter")
151140

152141
arity = len(signature.parameters)
153-
extract_io_type_hints(handler_io, signature)
142+
extract_io_type_hints(handler_io, signature) # mutates handler_io
154143

155144
handler = Handler[I, O](service_tag,
156145
handler_io,

python/restate/object.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import inspect
1818
import typing
1919

20-
from restate.serde import Serde, JsonSerde
20+
from restate.serde import Serde, GeneralSerde
2121
from .handler import HandlerIO, ServiceTag, make_handler
2222

2323
I = typing.TypeVar('I')
@@ -54,8 +54,8 @@ def handler(self,
5454
kind: typing.Optional[typing.Literal["exclusive", "shared"]] = "exclusive",
5555
accept: str = "application/json",
5656
content_type: str = "application/json",
57-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
58-
output_serde: Serde[O] = JsonSerde[O]()) -> typing.Callable: # type: ignore
57+
input_serde: Serde[I] = GeneralSerde[I](), # type: ignore
58+
output_serde: Serde[O] = GeneralSerde[O]()) -> typing.Callable: # type: ignore
5959
"""
6060
Decorator for defining a handler function.
6161

python/restate/serde.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@
1313
import json
1414
import typing
1515

16+
def try_import_pydantic_base_model():
17+
"""
18+
Try to import PydanticBaseModel from Pydantic.
19+
"""
20+
try:
21+
from pydantic import BaseModel # type: ignore # pylint: disable=import-outside-toplevel
22+
return BaseModel
23+
except ImportError:
24+
class Dummy: # pylint: disable=too-few-public-methods
25+
"""a dummy class to use when Pydantic is not available"""
26+
27+
return Dummy
28+
29+
PydanticBaseModel = try_import_pydantic_base_model()
30+
1631
T = typing.TypeVar('T')
1732
I = typing.TypeVar('I')
1833
O = typing.TypeVar('O')
@@ -107,6 +122,51 @@ def serialize(self, obj: typing.Optional[I]) -> bytes:
107122

108123
return bytes(json.dumps(obj), "utf-8")
109124

125+
class GeneralSerde(Serde[I]):
126+
"""
127+
A general serializer/deserializer that first checks if the object is a Pydantic BaseModel.
128+
If so, it uses the model's native JSON dumping method.
129+
Otherwise, it defaults to using the standard JSON library.
130+
"""
131+
132+
def deserialize(self, buf: bytes) -> typing.Optional[I]:
133+
"""
134+
Deserializes a byte array into a Python object.
135+
136+
Args:
137+
buf (bytes): The byte array to deserialize.
138+
139+
Returns:
140+
Optional[I]: The resulting Python object, or None if the input is empty.
141+
"""
142+
print("Deserializing using GeneralSerde")
143+
if not buf:
144+
return None
145+
print(f"json.loads(buf): {json.loads(buf)}")
146+
return json.loads(buf)
147+
148+
def serialize(self, obj: typing.Optional[I]) -> bytes:
149+
"""
150+
Serializes a Python object into a byte array.
151+
If the object is a Pydantic BaseModel, uses its model_dump_json method.
152+
153+
Args:
154+
obj (Optional[I]): The Python object to serialize.
155+
156+
Returns:
157+
bytes: The serialized byte array.
158+
"""
159+
160+
if obj is None:
161+
return bytes()
162+
163+
if isinstance(obj, PydanticBaseModel):
164+
# Use the Pydantic-specific serialization
165+
return obj.model_dump_json().encode("utf-8") # type: ignore[attr-defined]
166+
167+
# Fallback to standard JSON serialization
168+
return json.dumps(obj).encode("utf-8")
169+
110170

111171
class PydanticJsonSerde(Serde[I]):
112172
"""

python/restate/service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import inspect
1919
import typing
2020

21-
from restate.serde import Serde, JsonSerde
21+
from restate.serde import Serde, GeneralSerde
2222
from .handler import Handler, HandlerIO, ServiceTag, make_handler
2323

2424
I = typing.TypeVar('I')
@@ -54,8 +54,8 @@ def handler(self,
5454
name: typing.Optional[str] = None,
5555
accept: str = "application/json",
5656
content_type: str = "application/json",
57-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
58-
output_serde: Serde[O] = JsonSerde[O]()) -> typing.Callable: # type: ignore
57+
input_serde: Serde[I] = GeneralSerde[I](), # type: ignore
58+
output_serde: Serde[O] = GeneralSerde[O]()) -> typing.Callable: # type: ignore
5959
"""
6060
Decorator for defining a handler function.
6161

python/restate/workflow.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import inspect
1919
import typing
2020

21-
from restate.serde import Serde, JsonSerde
21+
from restate.serde import Serde, GeneralSerde
2222
from .handler import HandlerIO, ServiceTag, make_handler
2323

2424
I = typing.TypeVar('I')
@@ -57,8 +57,8 @@ def main(self,
5757
name: typing.Optional[str] = None,
5858
accept: str = "application/json",
5959
content_type: str = "application/json",
60-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
61-
output_serde: Serde[O] = JsonSerde[O]()) -> typing.Callable: # type: ignore
60+
input_serde: Serde[I] = GeneralSerde[I](), # type: ignore
61+
output_serde: Serde[O] = GeneralSerde[O]()) -> typing.Callable: # type: ignore
6262
"""Mark this handler as a workflow entry point"""
6363
return self._add_handler(name,
6464
kind="workflow",
@@ -71,8 +71,8 @@ def handler(self,
7171
name: typing.Optional[str] = None,
7272
accept: str = "application/json",
7373
content_type: str = "application/json",
74-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
75-
output_serde: Serde[O] = JsonSerde[O]()) -> typing.Callable: # type: ignore
74+
input_serde: Serde[I] = GeneralSerde[I](), # type: ignore
75+
output_serde: Serde[O] = GeneralSerde[O]()) -> typing.Callable: # type: ignore
7676
"""
7777
Decorator for defining a handler function.
7878
"""
@@ -83,8 +83,8 @@ def _add_handler(self,
8383
kind: typing.Literal["workflow", "shared", "exclusive"] = "shared",
8484
accept: str = "application/json",
8585
content_type: str = "application/json",
86-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
87-
output_serde: Serde[O] = JsonSerde[O]()) -> typing.Callable: # type: ignore
86+
input_serde: Serde[I] = GeneralSerde[I](), # type: ignore
87+
output_serde: Serde[O] = GeneralSerde[O]()) -> typing.Callable: # type: ignore
8888
"""
8989
Decorator for defining a handler function.
9090

0 commit comments

Comments
 (0)