Skip to content

feat: Type-aware Durable Functions payload serialization#343

Open
andystaples wants to merge 1 commit into
Azure:devfrom
andystaples:andystaples/df-strict-type
Open

feat: Type-aware Durable Functions payload serialization#343
andystaples wants to merge 1 commit into
Azure:devfrom
andystaples:andystaples/df-strict-type

Conversation

@andystaples
Copy link
Copy Markdown
Contributor

@andystaples andystaples commented May 14, 2026

Adds an opt-in, type-aware JSON codec for Durable Functions payloads in azure.functions._durable_functions, routes ActivityTriggerConverter through it, and replaces the on-demand importlib.import_module call in _deserialize_custom_object with a sys.modules lookup.

What changed

azure/functions/_durable_functions.py

New public API:

  • df_dumps(value) -> str — JSON-encode a value using the Durable Functions convention.
  • df_loads(s, expected_type=None) -> Any — JSON-decode a string, optionally validating against an expected Python type and using it to reconstruct custom objects.

New helper exposed for callers that build their own json.dumps invocations (e.g. orchestrator state encoders):

  • _get_serialize_default() — returns the default= callback to pass to json.dumps under the active typing mode.

Two operating modes, selected at runtime via the environment variable AZURE_FUNCTIONS_DURABLE_STRICT_TYPING (truthy values: 1, true, yes, case-insensitive):

  • Loose mode (default, env var unset): wire-format-identical to today. df_dumps is json.dumps(value, default=_serialize_custom_object). df_loads runs the existing object_hook path so nested custom objects continue to be reconstructed automatically. Passing expected_type adds a class/module check that logs a warning on mismatch but otherwise preserves the legacy decode path.
  • Strict mode (env var truthy): df_dumps only wraps the top-level custom object — __data__ is serialized as plain JSON with no default= hook, so to_json() is responsible for serializing nested custom objects explicitly. df_loads parses without an object_hook; deserializing a custom-object envelope requires the caller to pass expected_type, which is then used to call expected_type.from_json(__data__) directly. Type mismatches raise TypeError.

_serialize_custom_object is unchanged. _deserialize_custom_object now resolves the declared class via sys.modules.get(...) instead of importlib.import_module(...).

azure/functions/durable_functions.py

ActivityTriggerConverter:

  • decode now calls _durable_functions.df_loads(data.value) in place of the inline json.loads(..., object_hook=...).
  • encode now calls _durable_functions.df_dumps(obj) in place of the inline json.dumps(..., default=...).
  • Added an inline comment noting that strict-mode decode of custom-object payloads currently lacks an expected_type because InConverter.decode doesn't receive the function's parameter type annotation from the worker — flagged as a follow-up.

Other converters (OrchestrationTriggerConverter, EnitityTriggerConverter, DurableClientConverter) are not touched.

Behavior changes for existing callers

The wire format is unchanged. Default df_dumps output equals what json.dumps(value, default=_serialize_custom_object) produced before. The behavior deltas are:

  1. Class resolution at decode time. _deserialize_custom_object (and therefore df_loads / json.loads(..., object_hook=...)) now resolves the declared class via sys.modules.get(module_name) instead of importlib.import_module(module_name). A custom-object envelope whose module has not already been imported by the host process can no longer be deserialized.
  2. Exception types. Replacing import_module changes a couple of error types raised from the loose-mode decode path:
    • Module not present in sys.modulesValueError (previously ImportError / ModuleNotFoundError).
    • Class not found in a loaded module → AttributeError (preserved from the old getattr behavior; no change).
    • Class missing from_jsonTypeError (no change).
  3. New environment variable. AZURE_FUNCTIONS_DURABLE_STRICT_TYPING now influences this module. Hosts that happen to set it for unrelated reasons will get strict-mode behavior.

Strict mode is opt-in. With AZURE_FUNCTIONS_DURABLE_STRICT_TYPING unset, behavior matches today's contract apart from items (1) and (2) above.

Follow-up

ActivityTriggerConverter.decode cannot currently forward an expected_type to df_loads because the worker's InConverter.decode dispatch doesn't pass the function's parameter type annotation. Consequence: Custom objects are not supported at all in strict-type mode. Adding that plumbing in azure-functions-python-worker would let strict-mode activity decode fully reconstruct custom objects via from_json. Flagged inline in the converter for the library/worker owners' input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant