Skip to content

Commit cb75760

Browse files
berndverstBernd VerstCopilot
authored
[Azure Functions] Add opt-in Unicode JSON serialization (#292)
* feat(functions): add opt-in Unicode JSON serialization Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54e13dcd-4b0d-4c0b-ab5b-bbb958fd5792 * chore(functions): adopt released Unicode codec API Require azure-functions 2.4.0b1 and exercise its released opt-in codec directly. Align documentation and samples with the available shared-library API. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54e13dcd-4b0d-4c0b-ab5b-bbb958fd5792 --------- Co-authored-by: Bernd Verst <beverst@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54e13dcd-4b0d-4c0b-ab5b-bbb958fd5792
1 parent d9cf9e2 commit cb75760

15 files changed

Lines changed: 537 additions & 13 deletions

File tree

‎.github/workflows/durabletask-azurefunctions.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
needs: run-tests
5858
runs-on: ubuntu-latest
5959
# The end-to-end suite launches a real Azure Functions host, which requires
60-
# ``azure-functions>=2.3.0b2``. That build is published to PyPI and pulled
60+
# ``azure-functions>=2.4.0b1``. That build is published to PyPI and pulled
6161
# in as a declared dependency of ``azure-functions-durable``.
6262
# Run the suite in both payload-typing modes: the default (loose) codec and
6363
# strict typing (``AZURE_FUNCTIONS_DURABLE_STRICT_TYPING=1``), which requires

‎azure-functions-durable/CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
ADDED
11+
12+
- Added `DFApp.configure_serialization(ensure_ascii=False)` to opt in to compact
13+
Unicode JSON across durable clients, orchestration and entity workers, activity
14+
bindings, and registered blueprints. The default remains ASCII-escaped JSON.
15+
The opt-in uses the shared codec's `ensure_ascii` option available in
16+
`azure-functions` v2.4.0b1; unsupported SDKs fail explicitly at configuration time.
17+
18+
CHANGED
19+
20+
- Updated the minimum `azure-functions` dependency to v2.4.0b1.
21+
1022
## v2.0.0rc3
1123

1224
ADDED

‎azure-functions-durable/MIGRATION_GUIDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ later.
6666
Replace the 1.x package constraint in `requirements.txt`:
6767

6868
```text
69-
azure-functions>=2.3.0b2
69+
azure-functions>=2.4.0b1
7070
azure-functions-durable>=2.0.0b1,<3
7171
```
7272

‎azure-functions-durable/README.md‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,56 @@ Key capabilities include durable orchestrations and sub-orchestrations, durable
3232
timers, external events, durable entities, retries, versioning, durable HTTP
3333
calls (`context.call_http(...)`), recurring scheduled tasks, and history export.
3434

35+
## Unicode JSON serialization
36+
37+
By default, durable payloads preserve the shared `azure-functions` codec's
38+
ASCII-escaped JSON representation. With `azure-functions>=2.4.0b1`, opt in to
39+
literal Unicode JSON on the root `DFApp` at startup:
40+
41+
```python
42+
import azure.durable_functions as df
43+
44+
app = df.DFApp()
45+
app.configure_serialization(ensure_ascii=False)
46+
```
47+
48+
The shared `df_dumps(..., ensure_ascii=...)` API was released in
49+
[`azure-functions` 2.4.0b1](https://github.com/Azure/azure-functions-python-library/releases/tag/2.4.0b1),
50+
which is the minimum required shared SDK version. Dependency installation
51+
selects a compatible release automatically. If an older SDK is manually
52+
installed despite that requirement, configuring the opt-in raises `RuntimeError`
53+
rather than silently ignoring it. Leaving the option unset, or explicitly
54+
choosing `ensure_ascii=True`, preserves the existing JSON encoding.
55+
56+
There is one encoding policy per Python worker process. It applies to both
57+
synchronous and asynchronous durable clients, orchestration and entity workers,
58+
activity binding outputs, and all registered blueprints, including handlers
59+
created before configuration. Configure every scaled-out worker consistently
60+
before invocations. Repeating the same setting is allowed; a conflicting setting
61+
or changing the policy after the first durable JSON serialization raises
62+
`ValueError`.
63+
64+
Literal Unicode reduces escaping overhead for non-Latin text. For example,
65+
a JSON object containing 100,000 Cyrillic characters occupies approximately
66+
200,012 UTF-8 bytes rather than 600,012 ASCII-escaped bytes. Actual reduction
67+
depends on the payload. The shared codec preserves surrogate-code-unit and
68+
ASCII DEL escapes, custom-object envelopes, and strict/loose typing semantics.
69+
This option does not normalize Unicode or change the decoder; existing escaped
70+
payloads remain readable.
71+
72+
> [!WARNING]
73+
> Enabling or disabling this option changes serialized bytes for non-ASCII
74+
> text. Review applications deriving hashes, signatures, IDs, or replay
75+
> decisions from serialized JSON before rolling out or rolling back a change.
76+
> Do not change the setting inside orchestrator code or during invocations.
77+
78+
The policy applies before large-payload externalization, so uploads use the
79+
selected JSON encoding and size thresholds use its serialized UTF-8 bytes.
80+
Reduced escaping does not eliminate aggregate request limits or replace
81+
externalization. Protocol envelopes, reference tokens, management responses, and
82+
JSON serialized explicitly by application code are not reformatted by this
83+
setting.
84+
3585
## Large payloads
3686

3787
Configure a `durabletask.payload.PayloadStore` once at app startup to store large

‎azure-functions-durable/azure/durable_functions/decorators/durable_app.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,23 @@ class DFApp(Blueprint, FunctionRegister):
493493
Exports the decorators required to declare and index DF Function-types.
494494
"""
495495

496+
def configure_serialization(self, *, ensure_ascii: bool = True) -> None:
497+
"""Configure JSON encoding for this app and its registered blueprints.
498+
499+
The default preserves ASCII-escaped JSON. Set ``ensure_ascii=False``
500+
to emit ordinary Unicode literally with ``azure-functions>=2.4.0b1``.
501+
An older SDK raises RuntimeError when this option is requested.
502+
503+
Call at startup in every worker process, before durable payloads are
504+
serialized. Clients, orchestrations, entities, and activity bindings
505+
share one policy per process, including handlers created earlier.
506+
Repeating the same setting is allowed. A conflicting setting, or
507+
changing the policy after serialization starts, raises ValueError.
508+
"""
509+
from ..internal.serialization import configure_serialization
510+
511+
configure_serialization(ensure_ascii=ensure_ascii)
512+
496513
def configure_large_payloads(self, *, payload_store: PayloadStore) -> None:
497514
"""Enable payload externalization for this app and its blueprints.
498515

‎azure-functions-durable/azure/durable_functions/internal/converters.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
from azure.functions._durable_functions import (
3333
EntityContext,
3434
OrchestrationContext,
35-
df_dumps,
3635
df_loads,
3736
)
3837

@@ -43,6 +42,7 @@
4342
ORCHESTRATION_TRIGGER,
4443
)
4544
from .payloads import ActivityPayload, get_transport_payload_store
45+
from .serialization import serialize_json
4646

4747
_TriggerMetadata = Optional[Mapping[str, meta.Datum]]
4848

@@ -162,7 +162,7 @@ def encode(cls, obj: Any, *,
162162
if isinstance(obj, ActivityPayload):
163163
return meta.Datum(type='json', value=obj.value)
164164
try:
165-
result = df_dumps(obj)
165+
result = serialize_json(obj)
166166
except TypeError as e:
167167
raise ValueError(
168168
f'activity trigger output must be json serializable ({obj})') from e

‎azure-functions-durable/azure/durable_functions/internal/serialization.py‎

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,72 @@
1010
"""
1111

1212
from __future__ import annotations
13+
import inspect
14+
from threading import Lock
1315
from azure.functions._durable_functions import df_dumps, df_loads
1416

15-
from typing import Any, override
17+
from typing import Any, Callable, Protocol, TypeGuard, override
1618

1719
from durabletask.serialization import JsonDataConverter
1820

1921

22+
class _ConfigurableJsonEncoder(Protocol):
23+
def __call__(self, value: Any, *, ensure_ascii: bool = True) -> str:
24+
...
25+
26+
27+
_serialization_lock = Lock()
28+
_ensure_ascii: bool | None = None
29+
_unicode_encoder: _ConfigurableJsonEncoder | None = None
30+
31+
32+
def _supports_ensure_ascii(
33+
encoder: Callable[[Any], str]) -> TypeGuard[_ConfigurableJsonEncoder]:
34+
parameter = inspect.signature(encoder).parameters.get("ensure_ascii")
35+
return parameter is not None and parameter.kind in (
36+
inspect.Parameter.KEYWORD_ONLY,
37+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
38+
)
39+
40+
41+
def configure_serialization(*, ensure_ascii: object = True) -> None:
42+
"""Select one JSON encoding policy before durable payload serialization."""
43+
global _ensure_ascii, _unicode_encoder
44+
if not isinstance(ensure_ascii, bool):
45+
raise TypeError("ensure_ascii must be a bool")
46+
with _serialization_lock:
47+
if _ensure_ascii is not None and _ensure_ascii != ensure_ascii:
48+
raise ValueError(
49+
"JSON serialization is already configured with "
50+
f"ensure_ascii={_ensure_ascii} in this process; configure "
51+
"serialization once before serializing durable payloads")
52+
if not ensure_ascii:
53+
encoder = df_dumps
54+
if not _supports_ensure_ascii(encoder):
55+
raise RuntimeError(
56+
"ensure_ascii=False requires azure-functions>=2.4.0b1, whose "
57+
"df_dumps supports the ensure_ascii keyword argument. "
58+
"Install a compatible azure-functions SDK before opting in.")
59+
_unicode_encoder = encoder
60+
_ensure_ascii = ensure_ascii
61+
62+
63+
def serialize_json(value: Any) -> str:
64+
"""Apply the process-wide policy using the shared Functions JSON codec."""
65+
global _ensure_ascii
66+
with _serialization_lock:
67+
if _ensure_ascii is None:
68+
_ensure_ascii = True
69+
ensure_ascii = _ensure_ascii
70+
encoder = _unicode_encoder
71+
if ensure_ascii:
72+
# Older shared SDKs do not accept the optional keyword argument.
73+
return df_dumps(value)
74+
if encoder is None:
75+
raise RuntimeError("Unicode JSON serialization has no configured encoder")
76+
return encoder(value, ensure_ascii=False)
77+
78+
2079
class FunctionsDataConverter(JsonDataConverter):
2180
""":class:`DataConverter` that serializes via azure-functions' codec.
2281
@@ -38,7 +97,7 @@ class FunctionsDataConverter(JsonDataConverter):
3897
def serialize(self, value: Any) -> str | None:
3998
if value is None:
4099
return None
41-
return df_dumps(value)
100+
return serialize_json(value)
42101

43102
@override
44103
def deserialize(self, data: str | None, target_type: type | None = None) -> Any:

‎azure-functions-durable/pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ readme = "README.md"
3232
dependencies = [
3333
"durabletask[opentelemetry]>=1.12.0",
3434
"azure-identity>=1.19.0",
35-
"azure-functions>=2.3.0b2"
35+
"azure-functions>=2.4.0b1"
3636
]
3737

3838
[project.urls]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
azure-functions>=2.3.0b2
1+
azure-functions>=2.4.0b1
22
azure-functions-durable>=2.0.0b1,<3
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
azure-functions>=2.3.0b2
1+
azure-functions>=2.4.0b1
22
azure-functions-durable>=2.0.0b1,<3

0 commit comments

Comments
 (0)