From 6222a45ad3bf4a2f4041cb263244606ee2799206 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 27 Jul 2026 11:53:07 -0500 Subject: [PATCH 1/9] Fix inconsistent serialization for binding objects --- azure/functions/_abc.py | 54 +++++-- azure/functions/_blob.py | 12 ++ azure/functions/_cosmosdb.py | 5 +- azure/functions/_eventgrid.py | 50 ++++++ azure/functions/_eventhub.py | 18 +++ azure/functions/_kafka.py | 4 +- azure/functions/_mysql.py | 14 +- azure/functions/_queue.py | 18 +++ azure/functions/_servicebus.py | 41 +++++ azure/functions/_sql.py | 14 +- azure/functions/_timer.py | 6 + azure/functions/_utils.py | 29 +++- azure/functions/blob.py | 14 ++ azure/functions/kafka.py | 17 ++ azure/functions/timer.py | 8 + tests/test_serialization.py | 273 +++++++++++++++++++++++++++++++++ 16 files changed, 558 insertions(+), 19 deletions(-) create mode 100644 tests/test_serialization.py diff --git a/azure/functions/_abc.py b/azure/functions/_abc.py index 2f5cb692..61a53d86 100644 --- a/azure/functions/_abc.py +++ b/azure/functions/_abc.py @@ -4,6 +4,7 @@ import abc import datetime import io +import json import threading import typing @@ -12,6 +13,35 @@ T = typing.TypeVar('T') +class Serializable(abc.ABC): + """Mixin that provides a uniform serialization contract for binding types. + + Concrete binding classes must implement :meth:`to_dict`. A default + :meth:`to_json` implementation is provided that serializes the result of + :meth:`to_dict` to a JSON string. + """ + + @abc.abstractmethod + def to_dict(self) -> typing.Any: + """Return a JSON-safe representation of this binding object. + + Non-list binding types return a ``dict``; collection types such as + ``DocumentList`` or ``SqlRowList`` return a ``list`` of ``dict``. + All ``bytes`` values are decoded as UTF-8 when possible, otherwise + represented as ``{"__encoding": "base64", "value": ""}``. + All ``datetime`` values are represented as ISO-8601 strings. + """ + ... + + def to_json(self) -> str: + """Return the JSON string representation of this binding object. + + Delegates to :meth:`to_dict`; subclasses should override + :meth:`to_dict` rather than this method. + """ + return json.dumps(self.to_dict()) + + class Out(abc.ABC, typing.Generic[T]): """An interface to set function output parameters.""" @@ -215,7 +245,7 @@ def get_body(self) -> bytes: pass -class TimerRequest(abc.ABC): +class TimerRequest(Serializable): """Timer request object.""" @property @@ -225,7 +255,7 @@ def past_due(self) -> bool: pass -class InputStream(io.BufferedIOBase, abc.ABC): +class InputStream(io.BufferedIOBase, Serializable): """File-like object representing an input blob.""" @abc.abstractmethod @@ -261,7 +291,7 @@ def uri(self) -> typing.Optional[str]: pass -class QueueMessage(abc.ABC): +class QueueMessage(Serializable): @property @abc.abstractmethod @@ -302,7 +332,7 @@ def pop_receipt(self) -> typing.Optional[str]: pass -class EventGridEvent(abc.ABC): +class EventGridEvent(Serializable): @property @abc.abstractmethod def id(self) -> str: @@ -338,7 +368,7 @@ def data_version(self) -> str: pass -class EventGridOutputEvent(abc.ABC): +class EventGridOutputEvent(Serializable): @property @abc.abstractmethod def id(self) -> str: @@ -369,7 +399,7 @@ def data_version(self) -> str: pass -class CloudEvent(abc.ABC): +class CloudEvent(Serializable): """A CloudEvents v1.0 event message.""" @property @@ -417,7 +447,7 @@ def get_json(self) -> typing.Any: pass -class Document(abc.ABC): +class Document(Serializable): @classmethod @abc.abstractmethod @@ -437,20 +467,16 @@ def __getitem__(self, key): def __setitem__(self, key, value): pass - @abc.abstractmethod - def to_json(self) -> str: - pass - @abc.abstractmethod def to_dict(self) -> dict: pass -class DocumentList(abc.ABC): +class DocumentList(Serializable): pass -class EventHubEvent(abc.ABC): +class EventHubEvent(Serializable): @abc.abstractmethod def get_body(self) -> bytes: @@ -489,7 +515,7 @@ def body(self) -> str: pass -class ServiceBusMessage(abc.ABC): +class ServiceBusMessage(Serializable): @abc.abstractmethod def get_body(self) -> typing.Union[str, bytes]: diff --git a/azure/functions/_blob.py b/azure/functions/_blob.py index 2cdf6ac9..4ebd1bbd 100644 --- a/azure/functions/_blob.py +++ b/azure/functions/_blob.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import typing from typing import Optional from azure.functions import _abc as azf_abc @@ -53,3 +54,14 @@ def read(self, size=-1) -> bytes: Bytes read from the input stream. """ return self._io.read(size) + + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of blob metadata fields. + + Blob content is intentionally excluded; use :meth:`read` to access it. + """ + return { + 'name': self._name, + 'uri': self._uri, + 'length': self._length, + } diff --git a/azure/functions/_cosmosdb.py b/azure/functions/_cosmosdb.py index dcdf01c6..941313cf 100644 --- a/azure/functions/_cosmosdb.py +++ b/azure/functions/_cosmosdb.py @@ -46,4 +46,7 @@ def __repr__(self) -> str: class DocumentList(_abc.DocumentList, collections.UserList): "A ``UserList`` subclass containing a list of :class:`~Document` objects" - pass + + def to_dict(self): + """Return a list of JSON-safe dicts, one per :class:`~Document`.""" + return [doc.to_dict() for doc in self] diff --git a/azure/functions/_eventgrid.py b/azure/functions/_eventgrid.py index 9eb40929..49cd5c13 100644 --- a/azure/functions/_eventgrid.py +++ b/azure/functions/_eventgrid.py @@ -61,6 +61,22 @@ def __repr__(self) -> str: f'at 0x{id(self):0x}>' ) + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of all EventGrid event fields. + + ``event_time`` is represented as an ISO-8601 string. + """ + from ._utils import _serialize_value + return { + 'id': self.id, + 'topic': self.topic, + 'subject': self.subject, + 'event_type': self.event_type, + 'event_time': _serialize_value(self.event_time), + 'data_version': self.data_version, + 'data': self.get_json(), + } + class EventGridOutputEvent(azf_abc.EventGridOutputEvent): """An EventGrid event message.""" @@ -110,6 +126,21 @@ def __repr__(self) -> str: f'at 0x{id(self):0x}>' ) + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of all EventGrid output event fields. + + ``event_time`` is represented as an ISO-8601 string. + """ + from ._utils import _serialize_value + return { + 'id': self.id, + 'subject': self.subject, + 'event_type': self.event_type, + 'event_time': _serialize_value(self.event_time), + 'data_version': self.data_version, + 'data': self.get_json(), + } + class CloudEvent(azf_abc.CloudEvent): """A CloudEvents v1.0 event message.""" @@ -182,3 +213,22 @@ def __repr__(self) -> str: f'type={self.type} ' f'at 0x{id(self):0x}>' ) + + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of all CloudEvent fields. + + ``time`` is represented as an ISO-8601 string. + """ + from ._utils import _serialize_value + return { + 'id': self.id, + 'source': self.source, + 'type': self.type, + 'specversion': self.specversion, + 'subject': self.subject, + 'time': _serialize_value(self.time), + 'datacontenttype': self.datacontenttype, + 'dataschema': self.dataschema, + 'data': self.get_json(), + 'extension_attrs': self.extension_attrs, + } diff --git a/azure/functions/_eventhub.py b/azure/functions/_eventhub.py index 47b25e11..a18f6782 100644 --- a/azure/functions/_eventhub.py +++ b/azure/functions/_eventhub.py @@ -80,6 +80,24 @@ def metadata(self) -> typing.Optional[typing.Mapping[str, typing.Any]]: } return self._trigger_metadata_pyobj + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of all Event Hub event fields. + + ``body`` bytes are decoded as UTF-8 when valid, otherwise represented + as ``{"__encoding": "base64", "value": ""}``. + ``enqueued_time`` is an ISO-8601 string. + """ + from ._utils import _serialize_value + return { + 'body': _serialize_value(self.get_body()), + 'partition_key': self.partition_key, + 'sequence_number': self.sequence_number, + 'offset': self.offset, + 'enqueued_time': _serialize_value(self.enqueued_time), + 'iothub_metadata': (dict(self.iothub_metadata) + if self.iothub_metadata else None), + } + def __repr__(self) -> str: return ( f' bytes: diff --git a/azure/functions/_mysql.py b/azure/functions/_mysql.py index 1f6b4072..41483a5a 100644 --- a/azure/functions/_mysql.py +++ b/azure/functions/_mysql.py @@ -55,6 +55,10 @@ def to_json(self) -> str: """Return the JSON representation of the MySqlRow""" return json.dumps(dict(self)) + def to_dict(self) -> dict: + """Return the MySqlRow as a plain dict.""" + return dict(self) + def __getitem__(self, key): return collections.UserDict.__getitem__(self, key) @@ -69,4 +73,12 @@ def __repr__(self) -> str: class MySqlRowList(BaseMySqlRowList, collections.UserList): "A ''UserList'' subclass containing a list of :class:'~MySqlRow' objects" - pass + + def to_dict(self): + """Return a list of plain dicts, one per :class:`~MySqlRow`.""" + return [row.to_dict() for row in self] + + def to_json(self) -> str: + """Return the JSON representation of the MySqlRowList.""" + import json as _stdlib_json + return _stdlib_json.dumps(self.to_dict()) diff --git a/azure/functions/_queue.py b/azure/functions/_queue.py index 85d574ce..0923363b 100644 --- a/azure/functions/_queue.py +++ b/azure/functions/_queue.py @@ -88,6 +88,24 @@ def get_json(self) -> typing.Any: """ return json.loads(self.__body) + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of all message fields. + + ``body`` bytes are decoded as UTF-8 when valid, otherwise represented + as ``{"__encoding": "base64", "value": ""}``. + Datetime fields are ISO-8601 strings. + """ + from ._utils import _serialize_value + return { + 'id': self.id, + 'body': _serialize_value(self.get_body()), + 'pop_receipt': self.pop_receipt, + 'dequeue_count': self.dequeue_count, + 'expiration_time': _serialize_value(self.expiration_time), + 'insertion_time': _serialize_value(self.insertion_time), + 'time_next_visible': _serialize_value(self.time_next_visible), + } + def __repr__(self) -> str: return ( f'' diff --git a/azure/functions/_servicebus.py b/azure/functions/_servicebus.py index fb45c244..550abb78 100644 --- a/azure/functions/_servicebus.py +++ b/azure/functions/_servicebus.py @@ -464,3 +464,44 @@ def __set_body(self, body): def get_body(self) -> bytes: """Return message content as bytes.""" return self.__body + + def to_dict(self) -> Dict[str, Any]: + """Return a JSON-safe dictionary of all Service Bus message fields. + + ``body`` bytes are decoded as UTF-8 when valid, otherwise represented + as ``{"__encoding": "base64", "value": ""}``. + Datetime fields are ISO-8601 strings; ``time_to_live`` is a string + representation of the ``timedelta``. + """ + from ._utils import _serialize_value + ttl = self.time_to_live + return { + 'body': _serialize_value(self.get_body()), + 'application_properties': self.application_properties, + 'content_type': self.content_type, + 'correlation_id': self.correlation_id, + 'dead_letter_error_description': self.dead_letter_error_description, + 'dead_letter_reason': self.dead_letter_reason, + 'dead_letter_source': self.dead_letter_source, + 'delivery_count': self.delivery_count, + 'enqueued_sequence_number': self.enqueued_sequence_number, + 'enqueued_time_utc': _serialize_value(self.enqueued_time_utc), + 'expires_at_utc': _serialize_value(self.expires_at_utc), + 'label': self.label, + 'locked_until': _serialize_value(self.locked_until), + 'lock_token': self.lock_token, + 'message_id': self.message_id, + 'partition_key': self.partition_key, + 'reply_to': self.reply_to, + 'reply_to_session_id': self.reply_to_session_id, + 'scheduled_enqueue_time_utc': _serialize_value( + self.scheduled_enqueue_time_utc), + 'sequence_number': self.sequence_number, + 'session_id': self.session_id, + 'state': self.state, + 'subject': self.subject, + 'time_to_live': str(ttl) if ttl is not None else None, + 'to': self.to, + 'transaction_partition_key': self.transaction_partition_key, + 'user_properties': self.user_properties, + } diff --git a/azure/functions/_sql.py b/azure/functions/_sql.py index 166b3af4..e4b1d1e4 100644 --- a/azure/functions/_sql.py +++ b/azure/functions/_sql.py @@ -55,6 +55,10 @@ def to_json(self) -> str: """Return the JSON representation of the SqlRow""" return json.dumps(dict(self)) + def to_dict(self) -> dict: + """Return the SqlRow as a plain dict.""" + return dict(self) + def __getitem__(self, key): return collections.UserDict.__getitem__(self, key) @@ -69,4 +73,12 @@ def __repr__(self) -> str: class SqlRowList(BaseSqlRowList, collections.UserList): "A ''UserList'' subclass containing a list of :class:'~SqlRow' objects" - pass + + def to_dict(self): + """Return a list of plain dicts, one per :class:`~SqlRow`.""" + return [row.to_dict() for row in self] + + def to_json(self) -> str: + """Return the JSON representation of the SqlRowList.""" + import json as _stdlib_json + return _stdlib_json.dumps(self.to_dict()) diff --git a/azure/functions/_timer.py b/azure/functions/_timer.py index 191b14ca..98ccbcf4 100644 --- a/azure/functions/_timer.py +++ b/azure/functions/_timer.py @@ -18,3 +18,9 @@ def __init__(self, *, past_due: bool = False) -> None: def past_due(self) -> bool: """Whether the timer is past due.""" return self.__past_due + + def to_dict(self): + """Return a JSON-safe dictionary of timer request fields.""" + return { + 'past_due': self.past_due, + } diff --git a/azure/functions/_utils.py b/azure/functions/_utils.py index ebfe82ab..fbead3b9 100644 --- a/azure/functions/_utils.py +++ b/azure/functions/_utils.py @@ -1,10 +1,37 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import List, Tuple, Optional +import base64 +import datetime as _dt +from typing import Any, List, Tuple, Optional from datetime import datetime, timedelta +def _serialize_value(v: Any) -> Any: + """Normalize a single field value to a JSON-safe type. + + - ``bytes``: decoded as UTF-8 when valid, otherwise base64-encoded as + ``{"__encoding": "base64", "value": ""}``. + - ``datetime.datetime`` / ``datetime.date``: ISO-8601 string via + ``.isoformat()``. + - ``datetime.timedelta``: ``str(v)`` (e.g. ``"0:05:00"``). + - All other types: returned unchanged. + """ + if isinstance(v, bytes): + try: + return v.decode('utf-8') + except UnicodeDecodeError: + return { + '__encoding': 'base64', + 'value': base64.b64encode(v).decode('ascii'), + } + if isinstance(v, (_dt.datetime, _dt.date)): + return v.isoformat() + if isinstance(v, _dt.timedelta): + return str(v) + return v + + def try_parse_datetime_with_formats( datetime_str: str, datetime_formats: List[str] diff --git a/azure/functions/blob.py b/azure/functions/blob.py index 1893507a..f86eb17c 100644 --- a/azure/functions/blob.py +++ b/azure/functions/blob.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import io +import typing from typing import Any, Optional, Union from azure.functions import _abc as azf_abc @@ -56,6 +57,19 @@ def seekable(self) -> bool: def writable(self) -> bool: return False + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of blob metadata fields. + + Blob content is intentionally excluded; use :meth:`read` to access it. + """ + return { + 'name': self._name, + 'uri': self._uri, + 'length': self._length, + 'blob_properties': self._blob_properties, + 'metadata': self._metadata, + } + class BlobConverter(meta.InConverter, meta.OutConverter, diff --git a/azure/functions/kafka.py b/azure/functions/kafka.py index f71bb2c4..da989103 100644 --- a/azure/functions/kafka.py +++ b/azure/functions/kafka.py @@ -77,6 +77,23 @@ def metadata(self) -> typing.Optional[typing.Mapping[str, typing.Any]]: return self._trigger_metadata_pyobj + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of all Kafka event fields. + + ``body`` bytes are decoded as UTF-8 when valid, otherwise represented + as ``{"__encoding": "base64", "value": ""}``. + """ + from ._utils import _serialize_value + return { + 'body': _serialize_value(self.get_body()), + 'key': self.key, + 'offset': self.offset, + 'partition': self.partition, + 'topic': self.topic, + 'timestamp': self.timestamp, + 'headers': self.headers, + } + def __repr__(self) -> str: return ( f' dict: def schedule(self) -> dict: return self.__schedule + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of all timer request fields.""" + return { + 'past_due': self.past_due, + 'schedule_status': self.schedule_status, + 'schedule': self.schedule, + } + class TimerRequestConverter(meta.InConverter, binding='timerTrigger', trigger=True): diff --git a/tests/test_serialization.py b/tests/test_serialization.py new file mode 100644 index 00000000..9cf2cf82 --- /dev/null +++ b/tests/test_serialization.py @@ -0,0 +1,273 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Tests for the uniform to_dict() / to_json() serialization contract.""" + +import base64 +import datetime +import json +import unittest + +import azure.functions as func +from azure.functions._utils import _serialize_value + + +class TestSerializeValue(unittest.TestCase): + def test_bytes_utf8(self): + assert _serialize_value(b"hello") == "hello" + + def test_bytes_non_utf8(self): + raw = bytes([0xFF, 0xFE]) + result = _serialize_value(raw) + assert result == { + "__encoding": "base64", + "value": base64.b64encode(raw).decode("ascii"), + } + + def test_datetime(self): + dt = datetime.datetime(2024, 1, 15, 12, 30, 0) + assert _serialize_value(dt) == "2024-01-15T12:30:00" + + def test_timedelta(self): + td = datetime.timedelta(hours=1, minutes=30) + result = _serialize_value(td) + assert isinstance(result, str) + assert "1:30:00" in result + + def test_passthrough_str(self): + assert _serialize_value("hello") == "hello" + + def test_passthrough_int(self): + assert _serialize_value(42) == 42 + + def test_passthrough_none(self): + assert _serialize_value(None) is None + + +class TestQueueMessageToDict(unittest.TestCase): + def test_to_dict_defaults(self): + msg = func.QueueMessage() + d = msg.to_dict() + assert d["id"] is None + assert d["body"] == "" # b"" decodes to "" + assert d["pop_receipt"] is None + assert d["dequeue_count"] is None + + def test_to_dict_with_body(self): + msg = func.QueueMessage(id="abc", body=b"hello") + d = msg.to_dict() + assert d["id"] == "abc" + assert d["body"] == "hello" + + def test_to_dict_bytes_body_non_utf8(self): + raw = bytes([0xFF, 0xFE]) + msg = func.QueueMessage(body=raw) + d = msg.to_dict() + assert d["body"]["__encoding"] == "base64" + + def test_to_json_returns_string(self): + msg = func.QueueMessage(id="x", body=b"data") + s = msg.to_json() + assert isinstance(s, str) + parsed = json.loads(s) + assert parsed["id"] == "x" + assert parsed["body"] == "data" + + +class TestEventHubEventToDict(unittest.TestCase): + def test_to_dict_basic(self): + from azure.functions._eventhub import EventHubEvent + evt = EventHubEvent( + body=b"payload", + partition_key="pk1", + sequence_number=42, + offset="100", + enqueued_time=datetime.datetime(2024, 6, 1, 10, 0, 0), + ) + d = evt.to_dict() + assert d["body"] == "payload" + assert d["partition_key"] == "pk1" + assert d["sequence_number"] == 42 + assert d["offset"] == "100" + assert d["enqueued_time"] == "2024-06-01T10:00:00" + assert d["iothub_metadata"] is None + + def test_to_json_roundtrip(self): + from azure.functions._eventhub import EventHubEvent + evt = EventHubEvent(body=b"test", partition_key="pk") + parsed = json.loads(evt.to_json()) + assert parsed["body"] == "test" + assert parsed["partition_key"] == "pk" + + +class TestEventGridEventToDict(unittest.TestCase): + def test_to_dict(self): + evt = func.EventGridEvent( + id="evt1", + data={"key": "val"}, + topic="/subscriptions/sub/providers/Microsoft.Storage", + subject="/blobServices/default/containers/test", + event_type="Microsoft.Storage.BlobCreated", + event_time=datetime.datetime(2024, 1, 1, 0, 0, 0), + data_version="1.0", + ) + d = evt.to_dict() + assert d["id"] == "evt1" + assert d["topic"].startswith("/subscriptions") + assert d["event_time"] == "2024-01-01T00:00:00" + assert d["data"] == {"key": "val"} + + def test_to_json(self): + evt = func.EventGridEvent( + id="e2", + data={}, + topic="t", + subject="s", + event_type="et", + event_time=None, + data_version="1", + ) + parsed = json.loads(evt.to_json()) + assert parsed["id"] == "e2" + assert parsed["event_time"] is None + + +class TestServiceBusMessageToDict(unittest.TestCase): + def test_stub_to_dict(self): + # The stub base class (_servicebus.ServiceBusMessage) also has to_dict + msg = func.ServiceBusMessage(body=b"hello") + d = msg.to_dict() + assert d["body"] == "hello" + assert d["message_id"] == "" # stub default + + def test_to_json(self): + msg = func.ServiceBusMessage(body=b"msg") + parsed = json.loads(msg.to_json()) + assert parsed["body"] == "msg" + + +class TestTimerRequestToDict(unittest.TestCase): + def test_base_timer_to_dict(self): + # The base _timer.TimerRequest + timer = func.TimerRequest(past_due=True) + d = timer.to_dict() + assert d["past_due"] is True + + def test_to_json(self): + timer = func.TimerRequest() + parsed = json.loads(timer.to_json()) + assert parsed["past_due"] is False + + +class TestInputStreamToDict(unittest.TestCase): + def test_base_blob_to_dict(self): + blob = func.InputStream(name="myblob", uri="https://x/blob", length=42) + d = blob.to_dict() + assert d["name"] == "myblob" + assert d["uri"] == "https://x/blob" + assert d["length"] == 42 + + def test_to_json(self): + blob = func.InputStream(name="b", uri="u", length=10) + parsed = json.loads(blob.to_json()) + assert parsed["name"] == "b" + + +class TestDocumentToDict(unittest.TestCase): + def test_document_to_dict(self): + doc = func.Document.from_dict({"id": "1", "value": 42}) + d = doc.to_dict() + assert d == {"id": "1", "value": 42} + + def test_document_list_to_dict(self): + dl = func.DocumentList() + dl.append(func.Document.from_dict({"a": 1})) + dl.append(func.Document.from_dict({"b": 2})) + result = dl.to_dict() + assert result == [{"a": 1}, {"b": 2}] + + def test_document_list_to_json(self): + dl = func.DocumentList() + dl.append(func.Document.from_dict({"x": 10})) + parsed = json.loads(dl.to_json()) + assert parsed == [{"x": 10}] + + +class TestSqlRowToDict(unittest.TestCase): + def test_sql_row_to_dict(self): + row = func.SqlRow.from_dict({"col1": "a", "col2": 1}) + assert row.to_dict() == {"col1": "a", "col2": 1} + + def test_sql_row_list_to_dict(self): + rl = func.SqlRowList() + rl.append(func.SqlRow.from_dict({"k": "v"})) + assert rl.to_dict() == [{"k": "v"}] + + def test_sql_row_list_to_json(self): + rl = func.SqlRowList() + rl.append(func.SqlRow.from_dict({"n": 7})) + parsed = json.loads(rl.to_json()) + assert parsed == [{"n": 7}] + + +class TestMySqlRowToDict(unittest.TestCase): + def test_mysql_row_to_dict(self): + row = func.MySqlRow.from_dict({"col": "val"}) + assert row.to_dict() == {"col": "val"} + + def test_mysql_row_list_to_dict(self): + rl = func.MySqlRowList() + rl.append(func.MySqlRow.from_dict({"x": 5})) + assert rl.to_dict() == [{"x": 5}] + + def test_mysql_row_list_to_json(self): + rl = func.MySqlRowList() + rl.append(func.MySqlRow.from_dict({"y": 9})) + parsed = json.loads(rl.to_json()) + assert parsed == [{"y": 9}] + + +class TestKafkaEventToDict(unittest.TestCase): + def test_to_dict(self): + from azure.functions.kafka import KafkaEvent + evt = KafkaEvent( + body=b"message", + key="key1", + offset=5, + partition=0, + topic="my-topic", + timestamp="2024-01-01T00:00:00", + ) + d = evt.to_dict() + assert d["body"] == "message" + assert d["key"] == "key1" + assert d["topic"] == "my-topic" + + def test_to_json(self): + from azure.functions.kafka import KafkaEvent + evt = KafkaEvent(body=b"k", topic="t", key="k1", offset=0, partition=1, + timestamp="ts") + parsed = json.loads(evt.to_json()) + assert parsed["body"] == "k" + + +class TestCloudEventToDict(unittest.TestCase): + def test_to_dict(self): + evt = func.CloudEvent( + id="ce1", + source="https://example.com/source", + type="com.example.someevent", + specversion="1.0", + data={"key": "value"}, + time=datetime.datetime(2024, 3, 1, 12, 0, 0), + ) + d = evt.to_dict() + assert d["id"] == "ce1" + assert d["time"] == "2024-03-01T12:00:00" + assert d["data"] == {"key": "value"} + + def test_to_json(self): + evt = func.CloudEvent( + id="ce2", source="s", type="t", specversion="1.0", data=None + ) + parsed = json.loads(evt.to_json()) + assert parsed["id"] == "ce2" From dba500972263583e2cfe11e83eafd9fc57730024 Mon Sep 17 00:00:00 2001 From: hallvictoria <59299039+hallvictoria@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:21:04 -0500 Subject: [PATCH 2/9] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- azure/functions/_eventgrid.py | 6 +++--- azure/functions/_mysql.py | 3 ++- azure/functions/_servicebus.py | 4 ++-- azure/functions/_sql.py | 3 ++- azure/functions/_utils.py | 4 ++++ azure/functions/blob.py | 5 +++-- azure/functions/kafka.py | 2 +- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/azure/functions/_eventgrid.py b/azure/functions/_eventgrid.py index 49cd5c13..ff25f090 100644 --- a/azure/functions/_eventgrid.py +++ b/azure/functions/_eventgrid.py @@ -74,7 +74,7 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: 'event_type': self.event_type, 'event_time': _serialize_value(self.event_time), 'data_version': self.data_version, - 'data': self.get_json(), + 'data': _serialize_value(self.get_json()), } @@ -229,6 +229,6 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: 'time': _serialize_value(self.time), 'datacontenttype': self.datacontenttype, 'dataschema': self.dataschema, - 'data': self.get_json(), - 'extension_attrs': self.extension_attrs, + 'data': _serialize_value(self.get_json()), + 'extension_attrs': _serialize_value(self.extension_attrs), } diff --git a/azure/functions/_mysql.py b/azure/functions/_mysql.py index 41483a5a..88ca74de 100644 --- a/azure/functions/_mysql.py +++ b/azure/functions/_mysql.py @@ -57,7 +57,8 @@ def to_json(self) -> str: def to_dict(self) -> dict: """Return the MySqlRow as a plain dict.""" - return dict(self) + from ._utils import _serialize_value + return _serialize_value(dict(self)) def __getitem__(self, key): return collections.UserDict.__getitem__(self, key) diff --git a/azure/functions/_servicebus.py b/azure/functions/_servicebus.py index 550abb78..883a0d83 100644 --- a/azure/functions/_servicebus.py +++ b/azure/functions/_servicebus.py @@ -477,7 +477,7 @@ def to_dict(self) -> Dict[str, Any]: ttl = self.time_to_live return { 'body': _serialize_value(self.get_body()), - 'application_properties': self.application_properties, + 'application_properties': _serialize_value(self.application_properties), 'content_type': self.content_type, 'correlation_id': self.correlation_id, 'dead_letter_error_description': self.dead_letter_error_description, @@ -503,5 +503,5 @@ def to_dict(self) -> Dict[str, Any]: 'time_to_live': str(ttl) if ttl is not None else None, 'to': self.to, 'transaction_partition_key': self.transaction_partition_key, - 'user_properties': self.user_properties, + 'user_properties': _serialize_value(self.user_properties), } diff --git a/azure/functions/_sql.py b/azure/functions/_sql.py index e4b1d1e4..d52af76d 100644 --- a/azure/functions/_sql.py +++ b/azure/functions/_sql.py @@ -57,7 +57,8 @@ def to_json(self) -> str: def to_dict(self) -> dict: """Return the SqlRow as a plain dict.""" - return dict(self) + from ._utils import _serialize_value + return _serialize_value(dict(self)) def __getitem__(self, key): return collections.UserDict.__getitem__(self, key) diff --git a/azure/functions/_utils.py b/azure/functions/_utils.py index fbead3b9..54b295c3 100644 --- a/azure/functions/_utils.py +++ b/azure/functions/_utils.py @@ -29,6 +29,10 @@ def _serialize_value(v: Any) -> Any: return v.isoformat() if isinstance(v, _dt.timedelta): return str(v) + if isinstance(v, dict): + return {k: _serialize_value(val) for k, val in v.items()} + if isinstance(v, (list, tuple)): + return [_serialize_value(item) for item in v] return v diff --git a/azure/functions/blob.py b/azure/functions/blob.py index f86eb17c..a1b3cbe5 100644 --- a/azure/functions/blob.py +++ b/azure/functions/blob.py @@ -62,12 +62,13 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: Blob content is intentionally excluded; use :meth:`read` to access it. """ + from ._utils import _serialize_value return { 'name': self._name, 'uri': self._uri, 'length': self._length, - 'blob_properties': self._blob_properties, - 'metadata': self._metadata, + 'blob_properties': _serialize_value(self._blob_properties), + 'metadata': _serialize_value(self._metadata), } diff --git a/azure/functions/kafka.py b/azure/functions/kafka.py index da989103..178e05c8 100644 --- a/azure/functions/kafka.py +++ b/azure/functions/kafka.py @@ -91,7 +91,7 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: 'partition': self.partition, 'topic': self.topic, 'timestamp': self.timestamp, - 'headers': self.headers, + 'headers': _serialize_value(self.headers), } def __repr__(self) -> str: From 76305dd267fefd186c19e866066c0259b3d48201 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 27 Jul 2026 14:29:01 -0500 Subject: [PATCH 3/9] Address feedback --- azure/functions/_abc.py | 20 +++++++++++---- azure/functions/_eventgrid.py | 4 +-- azure/functions/_eventhub.py | 2 +- azure/functions/_mysql.py | 2 +- azure/functions/_queue.py | 2 +- azure/functions/_servicebus.py | 2 +- azure/functions/_sql.py | 2 +- azure/functions/blob.py | 2 +- azure/functions/kafka.py | 2 +- tests/test_serialization.py | 46 ++++++++++++++++++++++++++++++++++ 10 files changed, 69 insertions(+), 15 deletions(-) diff --git a/azure/functions/_abc.py b/azure/functions/_abc.py index 61a53d86..23fa68bc 100644 --- a/azure/functions/_abc.py +++ b/azure/functions/_abc.py @@ -16,12 +16,16 @@ class Serializable(abc.ABC): """Mixin that provides a uniform serialization contract for binding types. - Concrete binding classes must implement :meth:`to_dict`. A default - :meth:`to_json` implementation is provided that serializes the result of - :meth:`to_dict` to a JSON string. + Binding classes should override :meth:`to_dict` to return a JSON-safe + representation. A default :meth:`to_json` implementation is provided + that serializes the result of :meth:`to_dict` to a JSON string. + + :meth:`to_dict` is intentionally non-abstract so that downstream + subclasses of the binding ABCs that predate this contract are not broken + at instantiation time. Calling ``to_dict()`` on a class that has not + implemented it raises :exc:`NotImplementedError` at call time. """ - @abc.abstractmethod def to_dict(self) -> typing.Any: """Return a JSON-safe representation of this binding object. @@ -30,8 +34,14 @@ def to_dict(self) -> typing.Any: All ``bytes`` values are decoded as UTF-8 when possible, otherwise represented as ``{"__encoding": "base64", "value": ""}``. All ``datetime`` values are represented as ISO-8601 strings. + + Subclasses that do not override this method will raise + :exc:`NotImplementedError` at call time rather than at instantiation. """ - ... + raise NotImplementedError( + f"{type(self).__name__} does not implement to_dict(). " + "Override this method to enable serialization." + ) def to_json(self) -> str: """Return the JSON string representation of this binding object. diff --git a/azure/functions/_eventgrid.py b/azure/functions/_eventgrid.py index ff25f090..35b61014 100644 --- a/azure/functions/_eventgrid.py +++ b/azure/functions/_eventgrid.py @@ -5,6 +5,7 @@ import typing from azure.functions import _abc as azf_abc +from ._utils import _serialize_value class EventGridEvent(azf_abc.EventGridEvent): @@ -66,7 +67,6 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: ``event_time`` is represented as an ISO-8601 string. """ - from ._utils import _serialize_value return { 'id': self.id, 'topic': self.topic, @@ -131,7 +131,6 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: ``event_time`` is represented as an ISO-8601 string. """ - from ._utils import _serialize_value return { 'id': self.id, 'subject': self.subject, @@ -219,7 +218,6 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: ``time`` is represented as an ISO-8601 string. """ - from ._utils import _serialize_value return { 'id': self.id, 'source': self.source, diff --git a/azure/functions/_eventhub.py b/azure/functions/_eventhub.py index a18f6782..ef483ffe 100644 --- a/azure/functions/_eventhub.py +++ b/azure/functions/_eventhub.py @@ -6,6 +6,7 @@ from azure.functions import _abc as func_abc from azure.functions import meta +from ._utils import _serialize_value class EventHubEvent(func_abc.EventHubEvent): @@ -87,7 +88,6 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: as ``{"__encoding": "base64", "value": ""}``. ``enqueued_time`` is an ISO-8601 string. """ - from ._utils import _serialize_value return { 'body': _serialize_value(self.get_body()), 'partition_key': self.partition_key, diff --git a/azure/functions/_mysql.py b/azure/functions/_mysql.py index 88ca74de..501111f2 100644 --- a/azure/functions/_mysql.py +++ b/azure/functions/_mysql.py @@ -4,6 +4,7 @@ import collections from ._jsonutils import json +from ._utils import _serialize_value class BaseMySqlRow(abc.ABC): @@ -57,7 +58,6 @@ def to_json(self) -> str: def to_dict(self) -> dict: """Return the MySqlRow as a plain dict.""" - from ._utils import _serialize_value return _serialize_value(dict(self)) def __getitem__(self, key): diff --git a/azure/functions/_queue.py b/azure/functions/_queue.py index 0923363b..4f21596e 100644 --- a/azure/functions/_queue.py +++ b/azure/functions/_queue.py @@ -6,6 +6,7 @@ from . import _abc from ._jsonutils import json +from ._utils import _serialize_value class QueueMessage(_abc.QueueMessage): @@ -95,7 +96,6 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: as ``{"__encoding": "base64", "value": ""}``. Datetime fields are ISO-8601 strings. """ - from ._utils import _serialize_value return { 'id': self.id, 'body': _serialize_value(self.get_body()), diff --git a/azure/functions/_servicebus.py b/azure/functions/_servicebus.py index 883a0d83..e7f793f3 100644 --- a/azure/functions/_servicebus.py +++ b/azure/functions/_servicebus.py @@ -5,6 +5,7 @@ from typing import Optional, Dict, Any, Union from . import _abc +from ._utils import _serialize_value class ServiceBusMessage(_abc.ServiceBusMessage): @@ -473,7 +474,6 @@ def to_dict(self) -> Dict[str, Any]: Datetime fields are ISO-8601 strings; ``time_to_live`` is a string representation of the ``timedelta``. """ - from ._utils import _serialize_value ttl = self.time_to_live return { 'body': _serialize_value(self.get_body()), diff --git a/azure/functions/_sql.py b/azure/functions/_sql.py index d52af76d..b7bb89fb 100644 --- a/azure/functions/_sql.py +++ b/azure/functions/_sql.py @@ -4,6 +4,7 @@ import collections from ._jsonutils import json +from ._utils import _serialize_value class BaseSqlRow(abc.ABC): @@ -57,7 +58,6 @@ def to_json(self) -> str: def to_dict(self) -> dict: """Return the SqlRow as a plain dict.""" - from ._utils import _serialize_value return _serialize_value(dict(self)) def __getitem__(self, key): diff --git a/azure/functions/blob.py b/azure/functions/blob.py index a1b3cbe5..a404bc5c 100644 --- a/azure/functions/blob.py +++ b/azure/functions/blob.py @@ -8,6 +8,7 @@ from azure.functions import _abc as azf_abc from azure.functions import _blob as azf_blob from . import meta +from ._utils import _serialize_value class InputStream(azf_blob.InputStream): @@ -62,7 +63,6 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: Blob content is intentionally excluded; use :meth:`read` to access it. """ - from ._utils import _serialize_value return { 'name': self._name, 'uri': self._uri, diff --git a/azure/functions/kafka.py b/azure/functions/kafka.py index 178e05c8..68efa142 100644 --- a/azure/functions/kafka.py +++ b/azure/functions/kafka.py @@ -9,6 +9,7 @@ from ._jsonutils import json from ._kafka import AbstractKafkaEvent +from ._utils import _serialize_value class KafkaEvent(AbstractKafkaEvent): @@ -83,7 +84,6 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: ``body`` bytes are decoded as UTF-8 when valid, otherwise represented as ``{"__encoding": "base64", "value": ""}``. """ - from ._utils import _serialize_value return { 'body': _serialize_value(self.get_body()), 'key': self.key, diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 9cf2cf82..636b4b7a 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -271,3 +271,49 @@ def test_to_json(self): ) parsed = json.loads(evt.to_json()) assert parsed["id"] == "ce2" + + +class TestSerializableNonBreaking(unittest.TestCase): + """Verify that subclassing a binding ABC without implementing to_dict() + does not break instantiation - the error is deferred to call time.""" + + def _make_custom_queue_message(self): + """Return a minimal concrete subclass of _abc.QueueMessage that + implements only the pre-existing abstract surface, not to_dict().""" + import azure.functions._abc as azf_abc + + class CustomQueueMessage(azf_abc.QueueMessage): + @property + def id(self): return "x" + def get_body(self): return b"body" + def get_json(self): return "body" + @property + def dequeue_count(self): return None + @property + def expiration_time(self): return None + @property + def insertion_time(self): return None + @property + def time_next_visible(self): return None + @property + def pop_receipt(self): return None + + return CustomQueueMessage + + def test_subclass_without_to_dict_can_be_instantiated(self): + # Must not raise TypeError at instantiation time. + cls = self._make_custom_queue_message() + msg = cls() + assert msg.id == "x" + + def test_subclass_without_to_dict_raises_at_call_time(self): + cls = self._make_custom_queue_message() + msg = cls() + with self.assertRaises(NotImplementedError): + msg.to_dict() + + def test_subclass_without_to_dict_to_json_raises_at_call_time(self): + cls = self._make_custom_queue_message() + msg = cls() + with self.assertRaises(NotImplementedError): + msg.to_json() From a85ab292eaf0f77159fd82e04abdb5526ce5b199 Mon Sep 17 00:00:00 2001 From: hallvictoria <59299039+hallvictoria@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:56:32 -0500 Subject: [PATCH 4/9] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- azure/functions/_abc.py | 2 +- azure/functions/_mysql.py | 3 +-- azure/functions/_sql.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/azure/functions/_abc.py b/azure/functions/_abc.py index 23fa68bc..c87b1f97 100644 --- a/azure/functions/_abc.py +++ b/azure/functions/_abc.py @@ -4,7 +4,7 @@ import abc import datetime import io -import json +from ._jsonutils import json import threading import typing diff --git a/azure/functions/_mysql.py b/azure/functions/_mysql.py index 501111f2..70ea39d8 100644 --- a/azure/functions/_mysql.py +++ b/azure/functions/_mysql.py @@ -81,5 +81,4 @@ def to_dict(self): def to_json(self) -> str: """Return the JSON representation of the MySqlRowList.""" - import json as _stdlib_json - return _stdlib_json.dumps(self.to_dict()) + return json.dumps(self.to_dict()) diff --git a/azure/functions/_sql.py b/azure/functions/_sql.py index b7bb89fb..89c64c69 100644 --- a/azure/functions/_sql.py +++ b/azure/functions/_sql.py @@ -81,5 +81,4 @@ def to_dict(self): def to_json(self) -> str: """Return the JSON representation of the SqlRowList.""" - import json as _stdlib_json - return _stdlib_json.dumps(self.to_dict()) + return json.dumps(self.to_dict()) From c8c82a05080d51ba9875e84f2ca0e3239c3ef165 Mon Sep 17 00:00:00 2001 From: hallvictoria <59299039+hallvictoria@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:06:20 -0500 Subject: [PATCH 5/9] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- azure/functions/_eventgrid.py | 2 +- azure/functions/_mysql.py | 2 +- azure/functions/_sql.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/azure/functions/_eventgrid.py b/azure/functions/_eventgrid.py index 35b61014..3bbbf322 100644 --- a/azure/functions/_eventgrid.py +++ b/azure/functions/_eventgrid.py @@ -137,7 +137,7 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: 'event_type': self.event_type, 'event_time': _serialize_value(self.event_time), 'data_version': self.data_version, - 'data': self.get_json(), +'data': _serialize_value(self.get_json()), } diff --git a/azure/functions/_mysql.py b/azure/functions/_mysql.py index 70ea39d8..d2bac0a7 100644 --- a/azure/functions/_mysql.py +++ b/azure/functions/_mysql.py @@ -54,7 +54,7 @@ def from_dict(cls, dct: dict) -> 'BaseMySqlRow': def to_json(self) -> str: """Return the JSON representation of the MySqlRow""" - return json.dumps(dict(self)) + return json.dumps(self.to_dict()) def to_dict(self) -> dict: """Return the MySqlRow as a plain dict.""" diff --git a/azure/functions/_sql.py b/azure/functions/_sql.py index 89c64c69..a9769b4c 100644 --- a/azure/functions/_sql.py +++ b/azure/functions/_sql.py @@ -54,7 +54,7 @@ def from_dict(cls, dct: dict) -> 'BaseSqlRow': def to_json(self) -> str: """Return the JSON representation of the SqlRow""" - return json.dumps(dict(self)) + return json.dumps(self.to_dict()) def to_dict(self) -> dict: """Return the SqlRow as a plain dict.""" From b4b883c012ab7a9e18c3931dfb2fb162486e6966 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 27 Jul 2026 15:13:22 -0500 Subject: [PATCH 6/9] fix indentation --- azure/functions/_eventgrid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure/functions/_eventgrid.py b/azure/functions/_eventgrid.py index 3bbbf322..4fa1adaf 100644 --- a/azure/functions/_eventgrid.py +++ b/azure/functions/_eventgrid.py @@ -137,7 +137,7 @@ def to_dict(self) -> typing.Dict[str, typing.Any]: 'event_type': self.event_type, 'event_time': _serialize_value(self.event_time), 'data_version': self.data_version, -'data': _serialize_value(self.get_json()), + 'data': _serialize_value(self.get_json()), } From 2a94fdab24e2a2b47cae0a960ba86f53802c2670 Mon Sep 17 00:00:00 2001 From: hallvictoria <59299039+hallvictoria@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:29:44 -0500 Subject: [PATCH 7/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- azure/functions/timer.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/azure/functions/timer.py b/azure/functions/timer.py index 30b7a906..6ab2938a 100644 --- a/azure/functions/timer.py +++ b/azure/functions/timer.py @@ -27,13 +27,15 @@ def schedule_status(self) -> dict: def schedule(self) -> dict: return self.__schedule - def to_dict(self) -> typing.Dict[str, typing.Any]: - """Return a JSON-safe dictionary of all timer request fields.""" - return { - 'past_due': self.past_due, - 'schedule_status': self.schedule_status, - 'schedule': self.schedule, - } +def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of all timer request fields.""" + from ._utils import _serialize_value + + return { + 'past_due': self.past_due, + 'schedule_status': _serialize_value(self.schedule_status), + 'schedule': _serialize_value(self.schedule), + } class TimerRequestConverter(meta.InConverter, From eb8a07e8358149f1f872e64fef605530ae0e5335 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 27 Jul 2026 15:30:20 -0500 Subject: [PATCH 8/9] top-level import --- azure/functions/timer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure/functions/timer.py b/azure/functions/timer.py index 6ab2938a..ec46bb80 100644 --- a/azure/functions/timer.py +++ b/azure/functions/timer.py @@ -7,6 +7,7 @@ from azure.functions import _timer as azf_timer from . import meta from ._jsonutils import json +from ._utils import _serialize_value class TimerRequest(azf_timer.TimerRequest): @@ -29,7 +30,6 @@ def schedule(self) -> dict: def to_dict(self) -> typing.Dict[str, typing.Any]: """Return a JSON-safe dictionary of all timer request fields.""" - from ._utils import _serialize_value return { 'past_due': self.past_due, From 67535035df0ca5076251a6cdcf51df3e897d85df Mon Sep 17 00:00:00 2001 From: hallvictoria <59299039+hallvictoria@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:41:02 -0500 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- azure/functions/_utils.py | 2 ++ azure/functions/timer.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/azure/functions/_utils.py b/azure/functions/_utils.py index 54b295c3..1d9d401a 100644 --- a/azure/functions/_utils.py +++ b/azure/functions/_utils.py @@ -15,6 +15,8 @@ def _serialize_value(v: Any) -> Any: - ``datetime.datetime`` / ``datetime.date``: ISO-8601 string via ``.isoformat()``. - ``datetime.timedelta``: ``str(v)`` (e.g. ``"0:05:00"``). + - ``dict`` / ``list`` / ``tuple``: normalized recursively by applying this + function to nested values. - All other types: returned unchanged. """ if isinstance(v, bytes): diff --git a/azure/functions/timer.py b/azure/functions/timer.py index ec46bb80..7f1cc09c 100644 --- a/azure/functions/timer.py +++ b/azure/functions/timer.py @@ -28,14 +28,14 @@ def schedule_status(self) -> dict: def schedule(self) -> dict: return self.__schedule -def to_dict(self) -> typing.Dict[str, typing.Any]: - """Return a JSON-safe dictionary of all timer request fields.""" - - return { - 'past_due': self.past_due, - 'schedule_status': _serialize_value(self.schedule_status), - 'schedule': _serialize_value(self.schedule), - } + def to_dict(self) -> typing.Dict[str, typing.Any]: + """Return a JSON-safe dictionary of all timer request fields.""" + + return { + 'past_due': self.past_due, + 'schedule_status': _serialize_value(self.schedule_status), + 'schedule': _serialize_value(self.schedule), + } class TimerRequestConverter(meta.InConverter,