Summary
Trigger/input binding objects lack a consistent, machine-usable serialization contract. Some expose to_dict(), some expose to_json() (returning a str), and most expose neither. Any consumer that needs to turn a binding object into structured data (logging, diagnostics, message pipelines, or feeding an LLM prompt) must special-case every type, and the natural fallback — str(obj) — yields an unusable Python repr (<azure.functions.blob.InputStream object at 0x...>) that silently drops the entire payload.
The inconsistency today (azure-functions 2.2.0)
| Binding type |
to_dict() |
to_json() |
get_body() |
get_json() |
str(obj) result |
blob.InputStream |
❌ |
❌ |
❌ (read()) |
❌ |
<...InputStream object at 0x...> |
QueueMessage |
❌ |
❌ |
✅ |
✅ |
<azure.QueueMessage id=... > (body lost) |
ServiceBusMessage |
❌ |
❌ |
✅ |
❌ |
<...ServiceBusMessage object at 0x...> |
EventGridEvent |
❌ |
❌ |
❌ |
✅ |
<azure.EventGridEvent id=.. topic=.. > (data lost) |
EventHubEvent |
❌ |
❌ |
✅ |
❌ |
<azure.EventHubEvent partition_key=.. > (body lost) |
KafkaEvent |
❌ |
❌ |
✅ |
❌ |
<azure.KafkaEvent key=.. > (body lost) |
Document |
✅ |
✅ |
— |
— |
(works) |
DocumentList |
❌ |
❌ |
— |
— |
[<azure.Document ...>] |
SqlRow / MySqlRow |
❌ |
✅ (str) |
— |
— |
<SqlRow at 0x...> |
SqlRowList / MySqlRowList |
❌ |
❌ |
— |
— |
[<SqlRow ...>] |
TimerRequest |
❌ |
❌ |
— |
— |
<...TimerRequest object at 0x...> |
So four different conventions (or none) for the same conceptual operation: "give me this binding as JSON-safe data."
Impact
Downstream consumers must maintain a per-type adapter matrix and re-derive field names (name/uri/metadata, get_body(), get_json(), enqueued_time, partition_key, …), including bytes→text/base64 and datetime→ISO handling, purely to avoid the str() repr trap. This is fragile across SDK versions and duplicated in every project that needs it.
Proposal
Introduce a uniform, documented serialization contract implemented consistently across all binding types, e.g. one of:
- A
to_dict() -> dict (JSON-safe) on every binding object, with a companion to_json() -> str, consistent in name and return type (today to_json returns a str on rows but a JSON string elsewhere, and to_dict exists only on Document); or
- A shared
Serializable/SupportsToJson protocol (or a mixin base) that guarantees the method surface and centralizes bytes/datetime normalization.
Guidelines for the payload: bytes decoded as UTF-8 when valid else base64 (with an encoding marker), datetimes as ISO-8601, and blob bindings surfaced as name/uri/length/metadata without forcing a read() of content.
Context
Found while building an LLM-agent runtime on Azure Functions, where trigger data is serialized into the agent prompt. We worked around it with a per-type adapter layer, but a first-party contract would let all consumers drop the shim. Downstream reports: Azure/azure-functions-agents-runtime#85 and Azure/azure-functions-bucees-planning#1197.
Summary
Trigger/input binding objects lack a consistent, machine-usable serialization contract. Some expose
to_dict(), some exposeto_json()(returning astr), and most expose neither. Any consumer that needs to turn a binding object into structured data (logging, diagnostics, message pipelines, or feeding an LLM prompt) must special-case every type, and the natural fallback —str(obj)— yields an unusable Python repr (<azure.functions.blob.InputStream object at 0x...>) that silently drops the entire payload.The inconsistency today (azure-functions 2.2.0)
to_dict()to_json()get_body()get_json()str(obj)resultblob.InputStreamread())<...InputStream object at 0x...>QueueMessage<azure.QueueMessage id=... >(body lost)ServiceBusMessage<...ServiceBusMessage object at 0x...>EventGridEvent<azure.EventGridEvent id=.. topic=.. >(data lost)EventHubEvent<azure.EventHubEvent partition_key=.. >(body lost)KafkaEvent<azure.KafkaEvent key=.. >(body lost)DocumentDocumentList[<azure.Document ...>]SqlRow/MySqlRowstr)<SqlRow at 0x...>SqlRowList/MySqlRowList[<SqlRow ...>]TimerRequest<...TimerRequest object at 0x...>So four different conventions (or none) for the same conceptual operation: "give me this binding as JSON-safe data."
Impact
Downstream consumers must maintain a per-type adapter matrix and re-derive field names (
name/uri/metadata,get_body(),get_json(),enqueued_time,partition_key, …), including bytes→text/base64 and datetime→ISO handling, purely to avoid thestr()repr trap. This is fragile across SDK versions and duplicated in every project that needs it.Proposal
Introduce a uniform, documented serialization contract implemented consistently across all binding types, e.g. one of:
to_dict() -> dict(JSON-safe) on every binding object, with a companionto_json() -> str, consistent in name and return type (todayto_jsonreturns astron rows but a JSON string elsewhere, andto_dictexists only onDocument); orSerializable/SupportsToJsonprotocol (or a mixin base) that guarantees the method surface and centralizes bytes/datetime normalization.Guidelines for the payload: bytes decoded as UTF-8 when valid else base64 (with an encoding marker), datetimes as ISO-8601, and blob bindings surfaced as
name/uri/length/metadatawithout forcing aread()of content.Context
Found while building an LLM-agent runtime on Azure Functions, where trigger data is serialized into the agent prompt. We worked around it with a per-type adapter layer, but a first-party contract would let all consumers drop the shim. Downstream reports: Azure/azure-functions-agents-runtime#85 and Azure/azure-functions-bucees-planning#1197.