Skip to content

Commit ad78d56

Browse files
authored
Feature/general serde (#48)
* 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. * remove trailing white space * refactor: rename extract_io_type_hints to update_handler_io_with_type_hints for clarity * fix: support JsonSerde in update_handler_io_with_type_hints and clean up GeneralSerde logging * refactor: replace GeneralSerde with DefaultSerde across multiple files - Updated imports and default serializer/deserializer in **context**, handler, object, **server_context**, service, and workflow modules. - DefaultSerde provides a unified approach for JSON serialization/deserialization, ensuring compatibility with both Pydantic models and standard JSON objects. * doc formating to respect just checks * fix merge error * fix Just trainling space
1 parent 3cd0f0a commit ad78d56

File tree

7 files changed

+99
-40
lines changed

7 files changed

+99
-40
lines changed

python/restate/context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, Union
1919
import typing
2020
from datetime import timedelta
21-
from restate.serde import JsonSerde, Serde
21+
from restate.serde import DefaultSerde, JsonSerde, Serde
2222

2323
T = TypeVar('T')
2424
I = TypeVar('I')
@@ -120,7 +120,7 @@ def request(self) -> Request:
120120
def run(self,
121121
name: str,
122122
action: RunAction[T],
123-
serde: Serde[T] = JsonSerde(),
123+
serde: Serde[T] = DefaultSerde(),
124124
max_attempts: typing.Optional[int] = None,
125125
max_retry_duration: typing.Optional[timedelta] = None) -> RestateDurableFuture[T]:
126126
"""

python/restate/handler.py

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from typing import Any, Callable, Awaitable, Dict, Generic, Literal, Optional, TypeVar
2121

2222
from restate.exceptions import TerminalError
23-
from restate.serde import JsonSerde, Serde, PydanticJsonSerde
23+
from restate.serde import DefaultSerde, 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
"""
@@ -90,7 +74,7 @@ def is_pydantic(annotation) -> bool:
9074
return False
9175

9276

93-
def extract_io_type_hints(handler_io: HandlerIO[I, O], signature: Signature):
77+
def update_handler_io_with_type_hints(handler_io: HandlerIO[I, O], signature: Signature):
9478
"""
9579
Augment handler_io with additional information about the input and output types.
9680
@@ -105,15 +89,15 @@ def extract_io_type_hints(handler_io: HandlerIO[I, O], signature: Signature):
10589

10690
if is_pydantic(annotation):
10791
handler_io.input_type.is_pydantic = True
108-
if isinstance(handler_io.input_serde, JsonSerde): # type: ignore
92+
if isinstance(handler_io.input_serde, DefaultSerde): # type: ignore
10993
handler_io.input_serde = PydanticJsonSerde(annotation)
11094

11195
annotation = signature.return_annotation
11296
handler_io.output_type = TypeHint(annotation=annotation, is_pydantic=False)
11397

11498
if is_pydantic(annotation):
11599
handler_io.output_type.is_pydantic=True
116-
if isinstance(handler_io.output_serde, JsonSerde): # type: ignore
100+
if isinstance(handler_io.output_serde, DefaultSerde): # type: ignore
117101
handler_io.output_serde = PydanticJsonSerde(annotation)
118102

119103
# pylint: disable=R0902
@@ -157,7 +141,7 @@ def make_handler(service_tag: ServiceTag,
157141
raise ValueError("Handler must have at least one parameter")
158142

159143
arity = len(signature.parameters)
160-
extract_io_type_hints(handler_io, signature)
144+
update_handler_io_with_type_hints(handler_io, signature) # mutates handler_io
161145

162146
handler = Handler[I, O](service_tag=service_tag,
163147
handler_io=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, DefaultSerde
2121
from restate.handler import Handler, HandlerIO, ServiceTag, make_handler
2222

2323
I = typing.TypeVar('I')
@@ -60,8 +60,8 @@ def handler(self,
6060
kind: typing.Optional[typing.Literal["exclusive", "shared"]] = "exclusive",
6161
accept: str = "application/json",
6262
content_type: str = "application/json",
63-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
64-
output_serde: Serde[O] = JsonSerde[O](), # type: ignore
63+
input_serde: Serde[I] = DefaultSerde[I](), # type: ignore
64+
output_serde: Serde[O] = DefaultSerde[O](), # type: ignore
6565
metadata: typing.Optional[dict] = None) -> typing.Callable:
6666
"""
6767
Decorator for defining a handler function.

python/restate/serde.py

Lines changed: 74 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,65 @@ def serialize(self, obj: typing.Optional[I]) -> bytes:
107122

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

125+
class DefaultSerde(Serde[I]):
126+
"""
127+
The default serializer/deserializer used when no explicit type hints are provided.
128+
129+
Behavior:
130+
- Serialization:
131+
- If the object is an instance of Pydantic's `BaseModel`,
132+
it uses `model_dump_json()` for serialization.
133+
- Otherwise, it falls back to `json.dumps()`.
134+
- Deserialization:
135+
- Uses `json.loads()` to convert byte arrays into Python objects.
136+
- Does **not** automatically reconstruct Pydantic models;
137+
deserialized objects remain as generic JSON structures (dicts, lists, etc.).
138+
139+
Serde Selection:
140+
- When using the `@handler` decorator, if a function's type hints specify a Pydantic model,
141+
`PydanticJsonSerde` is automatically selected instead of `DefaultSerde`.
142+
- `DefaultSerde` is only used if no explicit type hints are provided.
143+
144+
This serde ensures compatibility with both structured (Pydantic) and unstructured JSON data,
145+
while allowing automatic serde selection based on type hints.
146+
"""
147+
148+
def deserialize(self, buf: bytes) -> typing.Optional[I]:
149+
"""
150+
Deserializes a byte array into a Python object.
151+
152+
Args:
153+
buf (bytes): The byte array to deserialize.
154+
155+
Returns:
156+
Optional[I]: The resulting Python object, or None if the input is empty.
157+
"""
158+
if not buf:
159+
return None
160+
return json.loads(buf)
161+
162+
def serialize(self, obj: typing.Optional[I]) -> bytes:
163+
"""
164+
Serializes a Python object into a byte array.
165+
If the object is a Pydantic BaseModel, uses its model_dump_json method.
166+
167+
Args:
168+
obj (Optional[I]): The Python object to serialize.
169+
170+
Returns:
171+
bytes: The serialized byte array.
172+
"""
173+
174+
if obj is None:
175+
return bytes()
176+
177+
if isinstance(obj, PydanticBaseModel):
178+
# Use the Pydantic-specific serialization
179+
return obj.model_dump_json().encode("utf-8") # type: ignore[attr-defined]
180+
181+
# Fallback to standard JSON serialization
182+
return json.dumps(obj).encode("utf-8")
183+
110184

111185
class PydanticJsonSerde(Serde[I]):
112186
"""

python/restate/server_context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from restate.context import DurablePromise, ObjectContext, Request, RestateDurableFuture, SendHandle
2222
from restate.exceptions import TerminalError
2323
from restate.handler import Handler, handler_from_callable, invoke_handler
24-
from restate.serde import BytesSerde, JsonSerde, Serde
24+
from restate.serde import BytesSerde, DefaultSerde, JsonSerde, Serde
2525
from restate.server_types import Receive, Send
2626
from restate.vm import Failure, Invocation, NotReady, SuspendedException, VMWrapper, RunRetryConfig # pylint: disable=line-too-long
2727
from restate.vm import DoProgressAnyCompleted, DoProgressCancelSignalReceived, DoProgressReadFromInput, DoProgressExecuteRun # pylint: disable=line-too-long
@@ -339,7 +339,7 @@ async def create_run_coroutine(self,
339339
def run(self,
340340
name: str,
341341
action: Callable[[], T] | Callable[[], Awaitable[T]],
342-
serde: Optional[Serde[T]] = JsonSerde(),
342+
serde: Optional[Serde[T]] = DefaultSerde(),
343343
max_attempts: Optional[int] = None,
344344
max_retry_duration: Optional[timedelta] = None) -> RestateDurableFuture[T]:
345345
assert serde is not None

python/restate/service.py

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

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

2424
I = typing.TypeVar('I')
2525
O = typing.TypeVar('O')
@@ -56,9 +56,10 @@ def handler(self,
5656
name: typing.Optional[str] = None,
5757
accept: str = "application/json",
5858
content_type: str = "application/json",
59-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
60-
output_serde: Serde[O] = JsonSerde[O](), # type: ignore
61-
metadata: typing.Optional[typing.Dict[str, str]] = None) -> typing.Callable:
59+
input_serde: Serde[I] = DefaultSerde[I](), # type: ignore
60+
output_serde: Serde[O] = DefaultSerde[O](), # type: ignore
61+
metadata: typing.Optional[typing.Dict[str, str]] = None) -> typing.Callable: # type: ignore
62+
6263
"""
6364
Decorator for defining a handler function.
6465

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 DefaultSerde, Serde
2222
from restate.handler import Handler, HandlerIO, ServiceTag, make_handler
2323

2424
I = typing.TypeVar('I')
@@ -59,8 +59,8 @@ def main(self,
5959
name: typing.Optional[str] = None,
6060
accept: str = "application/json",
6161
content_type: str = "application/json",
62-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
63-
output_serde: Serde[O] = JsonSerde[O](), # type: ignore
62+
input_serde: Serde[I] = DefaultSerde[I](), # type: ignore
63+
output_serde: Serde[O] = DefaultSerde[O](), # type: ignore
6464
metadata: typing.Optional[typing.Dict[str, str]] = None) -> typing.Callable: # type: ignore
6565
"""Mark this handler as a workflow entry point"""
6666
return self._add_handler(name,
@@ -75,8 +75,8 @@ def handler(self,
7575
name: typing.Optional[str] = None,
7676
accept: str = "application/json",
7777
content_type: str = "application/json",
78-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
79-
output_serde: Serde[O] = JsonSerde[O](), # type: ignore
78+
input_serde: Serde[I] = DefaultSerde[I](), # type: ignore
79+
output_serde: Serde[O] = DefaultSerde[O](), # type: ignore
8080
metadata: typing.Optional[typing.Dict[str, str]] = None) -> typing.Callable:
8181
"""
8282
Decorator for defining a handler function.
@@ -88,8 +88,8 @@ def _add_handler(self,
8888
kind: typing.Literal["workflow", "shared", "exclusive"] = "shared",
8989
accept: str = "application/json",
9090
content_type: str = "application/json",
91-
input_serde: Serde[I] = JsonSerde[I](), # type: ignore
92-
output_serde: Serde[O] = JsonSerde[O](), # type: ignore
91+
input_serde: Serde[I] = DefaultSerde[I](), # type: ignore
92+
output_serde: Serde[O] = DefaultSerde[O](), # type: ignore
9393
metadata: typing.Optional[typing.Dict[str, str]] = None) -> typing.Callable: # type: ignore
9494
"""
9595
Decorator for defining a handler function.

0 commit comments

Comments
 (0)