Skip to content

Commit 785aca6

Browse files
authored
Add handling for datetimes to the default Json Converter (#872)
* Add a new converter to the default DataConverter for datetimes * Move conversion into the json converter to allow nested datetimes * Add timezone test * Format and readme update
1 parent ee6f8d7 commit 785aca6

File tree

3 files changed

+53
-3
lines changed

3 files changed

+53
-3
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,11 @@ The default data converter supports converting multiple types including:
316316
* Iterables including ones JSON dump may not support by default, e.g. `set`
317317
* [IntEnum, StrEnum](https://docs.python.org/3/library/enum.html) based enumerates
318318
* [UUID](https://docs.python.org/3/library/uuid.html)
319+
* `datetime.datetime`
319320

320321
To use pydantic model instances, see [Pydantic Support](#pydantic-support).
321322

322-
`datetime.date`, `datetime.time`, and `datetime.datetime` can only be used with the Pydantic data converter.
323+
`datetime.date` and `datetime.time` can only be used with the Pydantic data converter.
323324

324325
Although workflows, updates, signals, and queries can all be defined with multiple input parameters, users are strongly
325326
encouraged to use a single `dataclass` or Pydantic model parameter, so that fields with defaults can be easily added

temporalio/converter.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,9 @@ def default(self, o: Any) -> Any:
500500
501501
See :py:meth:`json.JSONEncoder.default`.
502502
"""
503+
# Datetime support
504+
if isinstance(o, datetime):
505+
return o.isoformat()
503506
# Dataclass support
504507
if dataclasses.is_dataclass(o):
505508
return dataclasses.asdict(o)
@@ -1132,7 +1135,7 @@ async def decode_failure(
11321135
BinaryPlainPayloadConverter(),
11331136
JSONProtoPayloadConverter(),
11341137
BinaryProtoPayloadConverter(),
1135-
JSONPlainPayloadConverter(),
1138+
JSONPlainPayloadConverter(), # JSON Plain needs to remain in last because it throws on unknown types
11361139
)
11371140

11381141
DataConverter.default = DataConverter()
@@ -1399,6 +1402,15 @@ def value_to_type(
13991402
# Any or primitives
14001403
if hint is Any:
14011404
return value
1405+
elif hint is datetime:
1406+
if isinstance(value, str):
1407+
try:
1408+
return _get_iso_datetime_parser()(value)
1409+
except ValueError as err:
1410+
raise TypeError(f"Failed parsing datetime string: {value}") from err
1411+
elif isinstance(value, datetime):
1412+
return value
1413+
raise TypeError(f"Expected datetime or ISO8601 string, got {type(value)}")
14021414
elif hint is int or hint is float:
14031415
if not isinstance(value, (int, float)):
14041416
raise TypeError(f"Expected value to be int|float, was {type(value)}")

tests/test_converter.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import traceback
99
from collections import deque
1010
from dataclasses import dataclass
11-
from datetime import datetime, timezone
11+
from datetime import datetime, timedelta, timezone
1212
from enum import Enum, IntEnum
1313
from typing import (
1414
Any,
@@ -86,6 +86,11 @@ class MyDataClass:
8686
baz: SerializableEnum
8787

8888

89+
@dataclass
90+
class DatetimeClass:
91+
datetime: datetime
92+
93+
8994
async def test_converter_default():
9095
async def assert_payload(
9196
input,
@@ -178,6 +183,38 @@ async def assert_payload(
178183
type_hint=RawValue,
179184
)
180185

186+
# Without type hint, it is deserialized as a str
187+
await assert_payload(
188+
datetime(2020, 1, 1, 1, 1, 1),
189+
"json/plain",
190+
'"2020-01-01T01:01:01"',
191+
expected_decoded_input="2020-01-01T01:01:01",
192+
)
193+
194+
# With type hint, it is deserialized as a datetime
195+
await assert_payload(
196+
datetime(2020, 1, 1, 1, 1, 1, 1),
197+
"json/plain",
198+
'"2020-01-01T01:01:01.000001"',
199+
type_hint=datetime,
200+
)
201+
202+
# Timezones work
203+
await assert_payload(
204+
datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone(timedelta(hours=5))),
205+
"json/plain",
206+
'"2020-01-01T01:01:01+05:00"',
207+
type_hint=datetime,
208+
)
209+
210+
# Data class with datetime
211+
await assert_payload(
212+
DatetimeClass(datetime=datetime(2020, 1, 1, 1, 1, 1)),
213+
"json/plain",
214+
'{"datetime":"2020-01-01T01:01:01"}',
215+
type_hint=DatetimeClass,
216+
)
217+
181218

182219
def test_binary_proto():
183220
# We have to test this separately because by default it never encodes

0 commit comments

Comments
 (0)