diff --git a/features/data_converter/transfer_types/spec.md b/features/data_converter/transfer_types/spec.md new file mode 100644 index 00000000..3d96042d --- /dev/null +++ b/features/data_converter/transfer_types/spec.md @@ -0,0 +1,198 @@ +# Transfer Type Converter Behavioral Specification + +Last updated: 2026-09-02 + +## Motivation + +Transfer Type Conversion provides a way to translate to and from a Payload serialization friendly format, e.g. a protobuf object, to a more ergonomic or language idiomatic type. +TransferTypeConverters are added to the API facing type and specify the target "transfer type" which is then used by the configured Payload Converter serialize to/from a Payload. + +For example, given the protobuf definition + +```proto3 + message WorkflowExecution { + string workflow_id = 1; + string run_id = 2; + } + + message PauseRequest { + WorkflowExecution execution = 1; + string reason = 2; + } +``` + +Using a generated protobuf object in Python may force an awkward interface: + +```python +request = pause_pb2.PauseRequest(reason="maintenance") + +# This does not test presence. Protobuf returns an empty message. +assert request.execution is not None +assert request.execution.workflow_id == "" + +# You must use the protobuf specific inspection for presence. +if request.HasField("execution"): + print(request.execution.workflow_id) +else: + print("No workflow execution provided") +``` + +Adding a TransferTypeConverter allows you to construct a more idiomatic type: + +```python +@dataclass(frozen=True, slots=True) +class WorkflowExecution: + workflow_id: str + run_id: str + + +# Add the PauseRequestConverter that converts to and from pause_pb2.PauseRequest +@transfer_type_convertible(PauseRequestConverter) +@dataclass(frozen=True, slots=True) +class PauseRequest: + reason: str + execution: WorkflowExecution | None = None + + +request = PauseRequest(reason="maintenance") + +# Can use normal Python optional-value semantics: +if request.execution is not None: + pause_workflow(request.execution.workflow_id) +else: + print("No workflow execution provided") +``` + +## Terminology + +- **Model type:** The user-facing type associated with a transfer type converter. +- **Model value:** A value of the model type. +- **Transfer type:** The serializer-facing type produced from a model value. +- **Transfer value:** A value of the transfer type. +- **Transfer type converter:** The conversion logic to map between the model type and the transfer type. + +## Behaviors + +### Conversion wraps configured payload converters + +#### Encoding + +When encoding a value, the transfer type converter is applied to the model type **before** the payload converter so the payload converter receives the transfer type. The external storage processing and codec are applied as normal to the resulting payload. + +```mermaid +sequenceDiagram + actor App as Application + participant Client + participant Converter as Transfer type converter + participant Serializer as Payload converter + participant Codec as Codec & ExtStore + participant Server as Temporal Server + + App->>+Client: Send request containing model value + + Note over Client,Converter: The SDK finds the converter
associated with the model type + + Client->>+Converter: to_transfer_type(model value) + Converter-->>-Client: Transfer value + + Client->>+Serializer: to_payload(transfer value) + Serializer-->>-Client: payload + + Client-->+Codec: process(payloads) + Codec-->-Client: encoded payloads + + Client->>-Server: Send outgoing request + +``` + +1. Locate the transfer converter from the declared model type. +2. Invoke the transfer converter when one is present. +3. Pass the resulting transfer value to the configured payload converter. +4. Apply external storage processing and payload codecs according to the SDK's existing data-converter contract. + +#### Decoding + +When decoding a value, external storage processing and codec are applied as normal to the payload. Then the transfer type converter is applied to the transfer type **after** the payload converter so. + +```mermaid +sequenceDiagram + actor App as Application + participant Client + participant Converter as Transfer type converter + participant Serializer as Payload converter + participant Codec as ExtStore & Codec + participant Server as Temporal Server + + Server->>+Client: Send incoming response + + Client->>+Codec: process(encoded payloads) + Codec-->>-Client: payloads + + Note over Client,Converter: The SDK finds the converter
associated with the model type + + Client->>+Serializer: from_payload(payload, transfer type) + Serializer-->>-Client: Transfer value + + Client->>+Converter: from_transfer_type(transfer value, model type) + Converter-->>-Client: Model value + + Client-->>-App: Return response containing model value +``` + +1. Apply payload codecs and external storage retrieval. +2. Locate the transfer converter from the requested model type. +3. Ask the configured payload converter to decode the payload as the transfer type. +4. Invoke the transfer converter to reconstruct the requested model value. + +### Top-level values only + +Transfer conversion only applies to top-level values and does not recursively inspect fields for transfer types. + +Nested serialization is the responsibility of the configured payload converter. + +### At most one transfer step + +Transfer type converters are applied at most once. For example, given: + +- type `A` has transfer type converter `converterA` +- `converterA` produces the transfer type `B` +- type `B` has transfer type converter `converterB` + +When encoding type `A`, the SDK does not inspect the output type `B` for a transfer type converter and thus does not apply `converterB`. + +Similarly when decoding, the SDK will convert using only the transfer type encoder registered on the requested model type. + +### TransferTypeConverter Selection + +Converting to the transfer type should use the type hint for the target execution rather than the value's runtime type. For example: + +```java + // Given this workflow + @WorkflowMethod + void run(Animal animal); + + // And this invocation + Animal value = new Dog(); + workflow.run(value); +``` + +Here the transfer type converter associated with `Animal` should be used to +even if `Dog` also has a transfer type converter ensure decoding can match. +If the type hint does not have an associated transfer type converter, the +value is passed directly to the configured payload converter. + +Converting from the transfer type should also use the type hint for the target execution. If the type hint does not have an associated transfer type converter, +the result of the configured payload converter should be used directly. + +A transfer converter declaration applies only to the exact model type on which it is declared. Declarations are not inherited from a base type. + +A subclass or other derived type can declare its own converter independently of its base type. + +### TransferType Nullability Converter + +TransferTypeConverters should specify a non-null transfer type. + +### Failure conversion + +Failure conversion should use the transfer converter aware payload converter before +applying the configured payload converter to failure details.