Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ to include examples, links to docs, or any other relevant information.
OpenTelemetry's threading instrumentation (enabled by strands, among others) is active; a
context is now detached exactly when its token is still valid in the current
`contextvars.Context`, which it stays when a workflow resumes on another pool thread.

- Decoding a dataclass with `field(init=False)` fields no longer raises `TypeError` for an
unexpected keyword argument. Those fields are left out of the constructor call and their
serialized values are restored afterwards, including on frozen dataclasses.
### Security

## [1.34.0] - 2026-09-30
Expand Down
15 changes: 13 additions & 2 deletions temporalio/converter/_payload_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -992,24 +992,35 @@ def value_to_type(
fields = dataclasses.fields(hint)
field_hints = get_type_hints(hint)
field_values = {}
non_init_field_values = {}
for field in fields:
field_value = value.get(field.name, dataclasses.MISSING)
# We do not check whether field is required here. Rather, we let the
# attempted instantiation of the dataclass raise if a field is
# missing
if field_value is not dataclasses.MISSING:
try:
field_values[field.name] = value_to_type(
converted = value_to_type(
field_hints[field.name], field_value, custom_converters
)
except Exception as err:
raise TypeError(
f"Failed converting field {field.name} on dataclass {hint}"
) from err
if field.init:
field_values[field.name] = converted
else:
non_init_field_values[field.name] = converted
# Simply instantiate the dataclass. This will fail as expected when
# missing required fields.
# TODO(cretz): Want way to convert snake case to camel case?
return hint(**field_values)
obj = hint(**field_values)
# Fields with init=False can't be passed to __init__, but they are
# serialized, so restore them afterwards. object.__setattr__ is used
# because the dataclass may be frozen.
for name, field_value in non_init_field_values.items():
object.__setattr__(obj, name, field_value)
return obj

# Pydantic model instance
# Pydantic users should use Pydantic v2 with
Expand Down
37 changes: 37 additions & 0 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,43 @@ def test_json_frozenset_round_trip(hint: Any, value: frozenset[int]):
assert converted == value


@dataclass
class DataClassWithNonInitFields:
foo: str
computed: str = dataclasses.field(init=False)
counter: int = dataclasses.field(init=False, default=0)

def __post_init__(self) -> None:
self.computed = self.foo.upper()


@dataclass(frozen=True)
class FrozenDataClassWithNonInitField:
foo: str
counter: int = dataclasses.field(init=False, default=0)


def test_json_dataclass_non_init_fields_round_trip():
converter = JSONPlainPayloadConverter()

value = DataClassWithNonInitFields("foo")
value.counter = 3
payload = converter.to_payload(value)
assert payload
converted = converter.from_payload(payload, DataClassWithNonInitFields)
assert converted == value
assert converted.computed == "FOO"
assert converted.counter == 3

frozen_value = FrozenDataClassWithNonInitField("foo")
object.__setattr__(frozen_value, "counter", 3)
payload = converter.to_payload(frozen_value)
assert payload
frozen_converted = converter.from_payload(payload, FrozenDataClassWithNonInitField)
assert frozen_converted == frozen_value
assert frozen_converted.counter == 3


def test_json_nested_frozenset_round_trip():
converter = JSONPlainPayloadConverter()
value = {frozenset({1, 2}), frozenset({3})}
Expand Down
Loading