Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/fixed/aliased-armadillo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`temporalio.contrib.pydantic`: setting `ToJsonOptions` on the Pydantic payload converter no longer drops field aliases or serializes `inf`/`nan` as `null`; the JSON output now matches the default converter apart from the options themselves.
8 changes: 6 additions & 2 deletions temporalio/contrib/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ def __init__(
"""
if max_cached_type_adapters is not None and max_cached_type_adapters < 0:
raise ValueError("max_cached_type_adapters cannot be negative")
self._schema_serializer = SchemaSerializer(any_schema())
# Match the defaults of pydantic_core.to_json so that the options only
# change what they control
self._schema_serializer = SchemaSerializer(
any_schema(), config={"ser_json_inf_nan": "constants"}
)
self._to_json_options = to_json_options
self._type_adapter: functools._lru_cache_wrapper[TypeAdapter[Any]] = (
functools.lru_cache(maxsize=max_cached_type_adapters)(TypeAdapter)
Expand Down Expand Up @@ -107,7 +111,7 @@ def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None:
"""
data = (
self._schema_serializer.to_json(
value, exclude_unset=self._to_json_options.exclude_unset
value, by_alias=True, exclude_unset=self._to_json_options.exclude_unset
)
if self._to_json_options
else to_json(value)
Expand Down
42 changes: 42 additions & 0 deletions tests/contrib/pydantic/test_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,48 @@ class Model(BaseModel):
assert restored._type_adapter.cache_info().hits == 1


class AliasModel(BaseModel):
order_id: int = pydantic.Field(default=-1, alias="orderId")
note: str = ""
ratio: float = 1.0
wait: datetime.timedelta = datetime.timedelta(seconds=1)
data: bytes = b"data"


@pytest.mark.parametrize(
"value",
[
AliasModel(orderId=7),
float("inf"),
float("-inf"),
float("nan"),
[float("inf"), 1.5, float("nan")],
{"ratio": float("inf")},
{"wait": datetime.timedelta(minutes=1), "data": b"data"},
],
)
def test_to_json_options_wire_format_matches_default(value: object):
default_payload = PydanticJSONPlainPayloadConverter().to_payload(value)
for options in (
temporalio.contrib.pydantic.ToJsonOptions(),
temporalio.contrib.pydantic.ToJsonOptions(exclude_unset=False),
):
options_payload = PydanticJSONPlainPayloadConverter(options).to_payload(value)
assert default_payload is not None
assert options_payload is not None
assert options_payload.data == default_payload.data


def test_to_json_options_exclude_unset_keeps_aliases():
converter = PydanticJSONPlainPayloadConverter(
temporalio.contrib.pydantic.ToJsonOptions(exclude_unset=True)
)
payload = converter.to_payload(AliasModel(orderId=7))
assert payload is not None
assert payload.data == b'{"orderId":7}'
assert converter.from_payload(payload, AliasModel) == AliasModel(orderId=7)


async def test_pydantic_data_converter_pickle():
restored = pickle.loads(pickle.dumps(pydantic_data_converter))
value = {"a": 1}
Expand Down
Loading