Skip to content
Draft
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 @@ -7,6 +7,10 @@ All notable changes to this project will be documented in this file.
### Added

- Client operation correlation logging: `FunctionInvocationId` is now propagated via HTTP headers to the host for client operations, enabling correlation with host logs.
- Centralized JSON serialization module (`azure.durable_functions.models.utils.df_serialization`): all serialization/deserialization of user payloads (orchestrator inputs/outputs, activity arguments and results, sub-orchestrator payloads, entity inputs/outputs, and client inputs) now flows through `df_dumps` / `df_loads`, replacing scattered `json.dumps(…, default=_serialize_custom_object)` / `json.loads(…, object_hook=_deserialize_custom_object)` calls. The wire format is **unchanged** — builtins serialize to plain JSON and custom objects continue to use the `{"__class__", "__module__", "__data__"}` convention.
- Type-hint-driven validation via `df_loads(s, expected_type=...)`: when the V2 programming model provides a return-type annotation for an activity or sub-orchestrator, `df_loads` validates the deserialized payload against that type **before** the legacy `object_hook` fires, catching class/module mismatches early.
- **Strict typing mode** (opt-in via `AZURE_FUNCTIONS_DURABLE_STRICT_TYPING=1`): when enabled, `import_module` is never called on either encode or decode. On encode, `df_dumps` wraps only the top-level custom object — `to_json()` must return plain-JSON-serializable data (nested custom objects must be serialized explicitly). On decode, `df_loads` calls `expected_type.from_json(raw["__data__"])` directly; `df_loads` without `expected_type` raises `TypeError` for custom-object payloads. A `TypeError` is also raised on type mismatch.
- Return-type discovery for V2 decorated activities/sub-orchestrators (`azure.durable_functions.models.utils.type_discovery`): resolves the concrete return annotation from the user's registered function, used to supply `expected_type` to `df_loads`.

## 1.0.0b6

Expand Down
20 changes: 16 additions & 4 deletions azure/durable_functions/decorators/durable_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def decorator(entity_func):

return decorator

def _configure_orchestrator_callable(self, wrap) -> Callable:
def _configure_orchestrator_callable(self, wrap, input_type=None) -> Callable:
"""Obtain decorator to construct an Orchestrator class from a user-defined Function.

In the old programming model, this decorator's logic was unavoidable boilerplate
Expand All @@ -86,6 +86,9 @@ def _configure_orchestrator_callable(self, wrap) -> Callable:
----------
wrap: Callable
The next decorator to be applied.
input_type: Optional[type]
The expected type for orchestration input, forwarded from
the orchestration_trigger decorator.

Returns
-------
Expand All @@ -99,12 +102,16 @@ def decorator(orchestrator_func):

# invoke next decorator, with the Orchestrator as input
handle.__name__ = orchestrator_func.__name__
# Stash the decorator-declared input type so the runtime
# can feed it to df_loads via context.get_input().
handle._df_input_type = input_type
return wrap(handle)

return decorator

def orchestration_trigger(self, context_name: str,
orchestration: Optional[str] = None):
orchestration: Optional[str] = None,
input_type: Optional[type] = None):
"""Register an Orchestrator Function.

Parameters
Expand All @@ -114,8 +121,13 @@ def orchestration_trigger(self, context_name: str,
orchestration: Optional[str]
Name of Orchestrator Function.
The value is None by default, in which case the name of the method is used.
input_type: Optional[type]
The expected type for the orchestration input. When set,
``context.get_input()`` will use this type to decode the
input payload without consulting ``sys.modules``. A
call-site ``expected_type`` argument on ``get_input``
takes precedence over this value.
"""
@self._configure_orchestrator_callable
@self._configure_function_builder
def wrap(fb):

Expand All @@ -127,7 +139,7 @@ def decorator():

return decorator()

return wrap
return self._configure_orchestrator_callable(wrap, input_type=input_type)

def activity_trigger(self, input_name: str,
activity: Optional[str] = None):
Expand Down
39 changes: 31 additions & 8 deletions azure/durable_functions/models/DurableEntityContext.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Optional, Any, Dict, Tuple, List, Callable
from azure.functions._durable_functions import _deserialize_custom_object
from .utils.df_serialization import df_loads
import json


Expand Down Expand Up @@ -36,6 +36,7 @@ def __init__(self,
self._is_newly_constructed: bool = False

self._state: Any = state
self._state_is_raw: bool = False
self._input: Any = None
self._operation: Optional[str] = None
self._result: Any = None
Expand Down Expand Up @@ -109,10 +110,17 @@ def from_json(cls, json_str: str) -> Tuple['DurableEntityContext', List[Dict[str

serialized_state = json_dict["state"]
if serialized_state is not None:
json_dict["state"] = from_json_util(serialized_state)
# Keep the raw serialized form so get_state() can deserialize
# lazily with an expected_type supplied by the user.
json_dict["state"] = serialized_state
else:
json_dict["state"] = None

batch = json_dict.pop("batch")
return cls(**json_dict), batch
ctx = cls(**json_dict)
if serialized_state is not None:
ctx._state_is_raw = True
return ctx, batch

def set_state(self, state: Any) -> None:
"""Set the state of the entity.
Expand All @@ -127,19 +135,26 @@ def set_state(self, state: Any) -> None:
# should only serialize the state at the end of the batch
self._state = state

def get_state(self, initializer: Optional[Callable[[], Any]] = None) -> Any:
def get_state(self, initializer: Optional[Callable[[], Any]] = None,
expected_type: Optional[type] = None) -> Any:
"""Get the current state of this entity.

Parameters
----------
initializer: Optional[Callable[[], Any]]
A 0-argument function to provide an initial state. Defaults to None.
expected_type: Optional[type]
The type to decode the state as. When set, the codec uses
this type directly without consulting ``sys.modules``.

Returns
-------
Any
The current state of the entity
"""
if self._state is not None and self._state_is_raw:
self._state = from_json_util(self._state, expected_type=expected_type)
self._state_is_raw = False
state = self._state
if state is not None:
return state
Expand All @@ -149,9 +164,15 @@ def get_state(self, initializer: Optional[Callable[[], Any]] = None) -> Any:
state = initializer()
return state

def get_input(self) -> Any:
def get_input(self, expected_type: Optional[type] = None) -> Any:
"""Get the input for this operation.

Parameters
----------
expected_type: Optional[type]
The type to decode the input as. When set, the codec uses
this type directly without consulting ``sys.modules``.

Returns
-------
Any
Expand All @@ -160,7 +181,7 @@ def get_input(self) -> Any:
input_ = None
req_input = self._input
req_input = json.loads(req_input)
input_ = None if req_input is None else from_json_util(req_input)
input_ = None if req_input is None else df_loads(req_input, expected_type=expected_type)
return input_

def set_result(self, result: Any) -> None:
Expand All @@ -180,7 +201,7 @@ def destruct_on_exit(self) -> None:
self._state = None


def from_json_util(json_str: str) -> Any:
def from_json_util(json_str: str, expected_type: Optional[type] = None) -> Any:
"""Load an arbitrary datatype from its JSON representation.

The Out-of-proc SDK has a special JSON encoding strategy
Expand All @@ -192,10 +213,12 @@ def from_json_util(json_str: str) -> Any:
----------
json_str: str
A JSON-formatted string, from durable-extension
expected_type: Optional[type]
The type to decode the value as.

Returns
-------
Any:
The original datatype that was serialized
"""
return json.loads(json_str, object_hook=_deserialize_custom_object)
return df_loads(json_str, expected_type=expected_type)
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ..models.DurableOrchestrationBindings import DurableOrchestrationBindings
from .utils.http_utils import get_async_request, post_async_request, delete_async_request
from .utils.entity_utils import EntityId
from azure.functions._durable_functions import _serialize_custom_object
from .utils.df_serialization import df_dumps


class DurableOrchestrationClient:
Expand Down Expand Up @@ -633,7 +633,7 @@ def _get_json_input(client_input: object) -> Optional[str]:
If the JSON serialization failed, see `serialize_custom_object`
"""
if client_input is not None:
return json.dumps(client_input, default=_serialize_custom_object)
return df_dumps(client_input)
return None

@staticmethod
Expand Down
Loading
Loading