@@ -47,6 +47,7 @@ event loop. This means task management, sleep, cancellation, etc have all been d
4747- [ Usage] ( #usage )
4848 - [ Client] ( #client )
4949 - [ Data Conversion] ( #data-conversion )
50+ - [ Custom Type Data Conversion] ( #custom-type-data-conversion )
5051 - [ Workers] ( #workers )
5152 - [ Workflows] ( #workflows )
5253 - [ Definition] ( #definition )
@@ -268,21 +269,118 @@ The default data converter supports converting multiple types including:
268269 * Iterables including ones JSON dump may not support by default, e.g. ` set `
269270 * Any class with a ` dict() ` method and a static ` parse_obj() ` method, e.g.
270271 [ Pydantic models] ( https://pydantic-docs.helpmanual.io/usage/models )
271- * Note, this doesn't mean every Pydantic field can be converted, only fields which the data converter supports
272+ * The default data converter is deprecated for Pydantic models and will warn if used since not all fields work.
273+ See [ this sample] ( https://github.com/temporalio/samples-python/tree/main/pydantic_converter ) for the recommended
274+ approach.
272275 * [ IntEnum, StrEnum] ( https://docs.python.org/3/library/enum.html ) based enumerates
273276 * [ UUID] ( https://docs.python.org/3/library/uuid.html )
274277
275278This notably doesn't include any ` date ` , ` time ` , or ` datetime ` objects as they may not work across SDKs.
276279
280+ Users are strongly encouraged to use a single ` dataclass ` for parameter and return types so fields with defaults can be
281+ easily added without breaking compatibility.
282+
277283Classes with generics may not have the generics properly resolved. The current implementation, similar to Pydantic, does
278284not have generic type resolution. Users should use concrete types.
279285
286+ ##### Custom Type Data Conversion
287+
280288For converting from JSON, the workflow/activity type hint is taken into account to convert to the proper type. Care has
281289been taken to support all common typings including ` Optional ` , ` Union ` , all forms of iterables and mappings, ` NewType ` ,
282290etc in addition to the regular JSON values mentioned before.
283291
284- Users are strongly encouraged to use a single ` dataclass ` for parameter and return types so fields with defaults can be
285- easily added without breaking compatibility.
292+ Data converters contain a reference to a payload converter class that is used to convert to/from payloads/values. This
293+ is a class and not an instance because it is instantiated on every workflow run inside the sandbox. The payload
294+ converter is usually a ` CompositePayloadConverter ` which contains a multiple ` EncodingPayloadConverter ` s it uses to try
295+ to serialize/deserialize payloads. Upon serialization, each ` EncodingPayloadConverter ` is tried until one succeeds. The
296+ ` EncodingPayloadConverter ` provides an "encoding" string serialized onto the payload so that, upon deserialization, the
297+ specific ` EncodingPayloadConverter ` for the given "encoding" is used.
298+
299+ The default data converter uses the ` DefaultPayloadConverter ` which is simply a ` CompositePayloadConverter ` with a known
300+ set of default ` EncodingPayloadConverter ` s. To implement a custom encoding for a custom type, a new
301+ ` EncodingPayloadConverter ` can be created for the new type. For example, to support ` IPv4Address ` types:
302+
303+ ``` python
304+ class IPv4AddressEncodingPayloadConverter (EncodingPayloadConverter ):
305+ @ property
306+ def encoding (self ) -> str :
307+ return " text/ipv4-address"
308+
309+ def to_payload (self , value : Any) -> Optional[Payload]:
310+ if isinstance (value, ipaddress.IPv4Address):
311+ return Payload(
312+ metadata = {" encoding" : self .encoding.encode()},
313+ data = str (value).encode(),
314+ )
315+ else :
316+ return None
317+
318+ def from_payload (self , payload : Payload, type_hint : Optional[Type] = None ) -> Any:
319+ assert not type_hint or type_hint is ipaddress.IPv4Address
320+ return ipaddress.IPv4Address(payload.data.decode())
321+
322+ class IPv4AddressPayloadConverter (CompositePayloadConverter ):
323+ def __init__ (self ) -> None :
324+ # Just add ours as first before the defaults
325+ super ().__init__ (
326+ IPv4AddressEncodingPayloadConverter(),
327+ * DefaultPayloadConverter.default_encoding_payload_converters,
328+ )
329+
330+ my_data_converter = dataclasses.replace(
331+ DataConverter.default,
332+ payload_converter_class = IPv4AddressPayloadConverter,
333+ )
334+ ```
335+
336+ Imports are left off for brevity.
337+
338+ This is good for many custom types. However, sometimes you want to override the behavior of the just the existing JSON
339+ encoding payload converter to support a new type. It is already the last encoding data converter in the list, so it's
340+ the fall-through behavior for any otherwise unknown type. Customizing the existing JSON converter has the benefit of
341+ making the type work in lists, unions, etc.
342+
343+ The ` JSONPlainPayloadConverter ` uses the Python [ json] ( https://docs.python.org/3/library/json.html ) library with an
344+ advanced JSON encoder by default and a custom value conversion method to turn ` json.load ` ed values to their type hints.
345+ The conversion can be customized for serialization with a custom ` json.JSONEncoder ` and deserialization with a custom
346+ ` JSONTypeConverter ` . For example, to support ` IPv4Address ` types in existing JSON conversion:
347+
348+ ``` python
349+ class IPv4AddressJSONEncoder (AdvancedJSONEncoder ):
350+ def default (self , o : Any) -> Any:
351+ if isinstance (o, ipaddress.IPv4Address):
352+ return str (o)
353+ return super ().default(o)
354+ class IPv4AddressJSONTypeConverter (JSONTypeConverter ):
355+ def to_typed_value (
356+ self , hint : Type, value : Any
357+ ) -> Union[Optional[Any], _JSONTypeConverterUnhandled]:
358+ if issubclass (hint, ipaddress.IPv4Address):
359+ return ipaddress.IPv4Address(value)
360+ return JSONTypeConverter.Unhandled
361+
362+ class IPv4AddressPayloadConverter (CompositePayloadConverter ):
363+ def __init__ (self ) -> None :
364+ # Replace default JSON plain with our own that has our encoder and type
365+ # converter
366+ json_converter = JSONPlainPayloadConverter(
367+ encoder = IPv4AddressJSONEncoder,
368+ custom_type_converters = [IPv4AddressJSONTypeConverter()],
369+ )
370+ super ().__init__ (
371+ * [
372+ c if not isinstance (c, JSONPlainPayloadConverter) else json_converter
373+ for c in DefaultPayloadConverter.default_encoding_payload_converters
374+ ]
375+ )
376+
377+ my_data_converter = dataclasses.replace(
378+ DataConverter.default,
379+ payload_converter_class = IPv4AddressPayloadConverter,
380+ )
381+ ```
382+
383+ Now ` IPv4Address ` can be used in type hints including collections, optionals, etc.
286384
287385### Workers
288386
0 commit comments