From 1141db15322f1b95d940193207c2eb8119ab4599 Mon Sep 17 00:00:00 2001 From: Abhishek Chauhan Date: Mon, 30 Mar 2026 11:41:34 -0500 Subject: [PATCH 1/2] fix(idempotency): serialize Pydantic models with mode='json' for UUID/date support The `_prepare_data()` function was calling `model_dump()` without specifying `mode="json"`, which defaults to `mode="python"`. This caused Pydantic models containing UUIDs, dates, or datetimes to fail with "Object of type UUID is not JSON serializable" when used with `@idempotent_function`. Fixes #8065 --- .../utilities/idempotency/base.py | 2 +- .../test_idempotency_uuid_serialization.py | 157 ++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 tests/functional/idempotency/_pydantic/test_idempotency_uuid_serialization.py diff --git a/aws_lambda_powertools/utilities/idempotency/base.py b/aws_lambda_powertools/utilities/idempotency/base.py index f93b9097611..f6a3563c103 100644 --- a/aws_lambda_powertools/utilities/idempotency/base.py +++ b/aws_lambda_powertools/utilities/idempotency/base.py @@ -60,7 +60,7 @@ def _prepare_data(data: Any) -> Any: # Convert from Pydantic model if callable(getattr(data, "model_dump", None)): - return data.model_dump() + return data.model_dump(mode="json") # Convert from event source data class if callable(getattr(data, "dict", None)): diff --git a/tests/functional/idempotency/_pydantic/test_idempotency_uuid_serialization.py b/tests/functional/idempotency/_pydantic/test_idempotency_uuid_serialization.py new file mode 100644 index 00000000000..65e8748ca06 --- /dev/null +++ b/tests/functional/idempotency/_pydantic/test_idempotency_uuid_serialization.py @@ -0,0 +1,157 @@ +""" +Test for issue #8065: @idempotent_function fails with UUID in Pydantic model + +Bug: _prepare_data() calls model_dump() without mode="json", which doesn't +serialize UUIDs and dates to JSON-compatible strings. +""" + +from datetime import date, datetime +from uuid import UUID, uuid4 + +import pytest +from pydantic import BaseModel + +from aws_lambda_powertools.utilities.idempotency import ( + IdempotencyConfig, + idempotent_function, +) +from aws_lambda_powertools.utilities.idempotency.base import _prepare_data +from aws_lambda_powertools.utilities.idempotency.persistence.base import ( + BasePersistenceLayer, + DataRecord, +) +from tests.functional.idempotency.utils import hash_idempotency_key + + +TESTS_MODULE_PREFIX = "test-func.tests.functional.idempotency._pydantic.test_idempotency_uuid_serialization" + + +class MockPersistenceLayer(BasePersistenceLayer): + """Mock persistence layer that tracks idempotency key assertions.""" + + def __init__(self, expected_idempotency_key: str): + self.expected_idempotency_key = expected_idempotency_key + super().__init__() + + def _put_record(self, data_record: DataRecord) -> None: + assert data_record.idempotency_key == self.expected_idempotency_key + + def _update_record(self, data_record: DataRecord) -> None: + assert data_record.idempotency_key == self.expected_idempotency_key + + def _get_record(self, idempotency_key) -> DataRecord: + ... + + def _delete_record(self, data_record: DataRecord) -> None: + ... + + +class PaymentWithUUID(BaseModel): + """Pydantic model with UUID field - reproduces issue #8065.""" + + payment_id: UUID + customer_id: str + + +class EventWithDate(BaseModel): + """Pydantic model with date field.""" + + event_id: str + event_date: date + + +class OrderWithDatetime(BaseModel): + """Pydantic model with datetime field.""" + + order_id: str + created_at: datetime + + +def test_prepare_data_pydantic_with_uuid(): + """ + Test that _prepare_data correctly serializes Pydantic models with UUID fields. + + Issue #8065: model_dump() without mode="json" returns UUID objects instead of strings, + which causes "Object of type UUID is not JSON serializable" error. + """ + # GIVEN a Pydantic model with UUID + payment_uuid = uuid4() + payment = PaymentWithUUID(payment_id=payment_uuid, customer_id="customer-123") + + # WHEN preparing data for idempotency + result = _prepare_data(payment) + + # THEN UUID should be serialized as string (not UUID object) + assert isinstance(result, dict) + assert isinstance(result["payment_id"], str), ( + f"UUID should be serialized as string, got {type(result['payment_id'])}" + ) + assert result["payment_id"] == str(payment_uuid) + assert result["customer_id"] == "customer-123" + + +def test_prepare_data_pydantic_with_date(): + """Test that _prepare_data correctly serializes Pydantic models with date fields.""" + # GIVEN a Pydantic model with date + event_date = date(2024, 1, 15) + event = EventWithDate(event_id="event-123", event_date=event_date) + + # WHEN preparing data for idempotency + result = _prepare_data(event) + + # THEN date should be serialized as ISO format string + assert isinstance(result, dict) + assert isinstance(result["event_date"], str), ( + f"date should be serialized as string, got {type(result['event_date'])}" + ) + assert result["event_date"] == "2024-01-15" + + +def test_prepare_data_pydantic_with_datetime(): + """Test that _prepare_data correctly serializes Pydantic models with datetime fields.""" + # GIVEN a Pydantic model with datetime + created_at = datetime(2024, 1, 15, 10, 30, 0) + order = OrderWithDatetime(order_id="order-123", created_at=created_at) + + # WHEN preparing data for idempotency + result = _prepare_data(order) + + # THEN datetime should be serialized as ISO format string + assert isinstance(result, dict) + assert isinstance(result["created_at"], str), ( + f"datetime should be serialized as string, got {type(result['created_at'])}" + ) + + +def test_idempotent_function_with_uuid_in_pydantic_model(): + """ + Integration test for idempotent_function with UUID in Pydantic model. + + This is the main test case for issue #8065. + """ + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + payment_uuid = UUID("12345678-1234-5678-1234-567812345678") + mock_event = {"payment_id": str(payment_uuid), "customer_id": "customer-456"} + + idempotency_key = ( + f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_uuid_in_pydantic_model" + f"..process_payment#{hash_idempotency_key(mock_event)}" + ) + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="payment", + persistence_store=persistence_layer, + config=config, + ) + def process_payment(payment: PaymentWithUUID) -> dict: + return {"status": "processed", "payment_id": str(payment.payment_id)} + + # WHEN processing payment with UUID + payment = PaymentWithUUID(payment_id=payment_uuid, customer_id="customer-456") + + # THEN it should not raise "Object of type UUID is not JSON serializable" + result = process_payment(payment=payment) + assert result["status"] == "processed" + assert result["payment_id"] == str(payment_uuid) From 8264fb1201635b4ca1c06a0ed6a24d7edc75d0b2 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 2 Apr 2026 15:21:07 +0100 Subject: [PATCH 2/2] Fix tests --- ...idempotency_pydantic_json_serialization.py | 185 ++++++++++++++++++ .../test_idempotency_uuid_serialization.py | 157 --------------- 2 files changed, 185 insertions(+), 157 deletions(-) create mode 100644 tests/functional/idempotency/_pydantic/test_idempotency_pydantic_json_serialization.py delete mode 100644 tests/functional/idempotency/_pydantic/test_idempotency_uuid_serialization.py diff --git a/tests/functional/idempotency/_pydantic/test_idempotency_pydantic_json_serialization.py b/tests/functional/idempotency/_pydantic/test_idempotency_pydantic_json_serialization.py new file mode 100644 index 00000000000..624e4239e98 --- /dev/null +++ b/tests/functional/idempotency/_pydantic/test_idempotency_pydantic_json_serialization.py @@ -0,0 +1,185 @@ +""" +Test for issue #8065: @idempotent_function fails with non-JSON-serializable types in Pydantic models + +Bug: _prepare_data() calls model_dump() without mode="json", which doesn't +serialize UUIDs, dates, datetimes, Decimals, and Paths to JSON-compatible strings. +""" + +from datetime import date, datetime +from decimal import Decimal +from pathlib import PurePosixPath +from uuid import UUID + +from pydantic import BaseModel + +from aws_lambda_powertools.utilities.idempotency import ( + IdempotencyConfig, + idempotent_function, +) +from aws_lambda_powertools.utilities.idempotency.persistence.base import ( + BasePersistenceLayer, + DataRecord, +) +from tests.functional.idempotency.utils import hash_idempotency_key + +TESTS_MODULE_PREFIX = "test-func.tests.functional.idempotency._pydantic.test_idempotency_pydantic_json_serialization" + + +class MockPersistenceLayer(BasePersistenceLayer): + def __init__(self, expected_idempotency_key: str): + self.expected_idempotency_key = expected_idempotency_key + super().__init__() + + def _put_record(self, data_record: DataRecord) -> None: + assert data_record.idempotency_key == self.expected_idempotency_key + + def _update_record(self, data_record: DataRecord) -> None: + assert data_record.idempotency_key == self.expected_idempotency_key + + def _get_record(self, idempotency_key) -> DataRecord: ... + + def _delete_record(self, data_record: DataRecord) -> None: ... + + +# --- Models --- + + +class PaymentWithUUID(BaseModel): + payment_id: UUID + customer_id: str + + +class EventWithDate(BaseModel): + event_id: str + event_date: date + + +class OrderWithDatetime(BaseModel): + order_id: str + created_at: datetime + + +class InvoiceWithDecimal(BaseModel): + invoice_id: str + amount: Decimal + + +class ConfigWithPath(BaseModel): + config_id: str + file_path: PurePosixPath + + +def test_idempotent_function_with_uuid(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + payment_uuid = UUID("12345678-1234-5678-1234-567812345678") + mock_event = {"payment_id": str(payment_uuid), "customer_id": "c-456"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_uuid..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="payment", + persistence_store=persistence_layer, + config=config, + ) + def collect_payment(payment: PaymentWithUUID) -> dict: + return {"status": "ok"} + + # WHEN + payment = PaymentWithUUID(payment_id=payment_uuid, customer_id="c-456") + result = collect_payment(payment=payment) + + # THEN + assert result == {"status": "ok"} + + +def test_idempotent_function_with_date(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"event_id": "evt-001", "event_date": "2024-03-20"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_date..process_event#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="event", + persistence_store=persistence_layer, + config=config, + ) + def process_event(event: EventWithDate) -> dict: + return {"status": "ok"} + + # WHEN + event = EventWithDate(event_id="evt-001", event_date=date(2024, 3, 20)) + result = process_event(event=event) + + # THEN + assert result == {"status": "ok"} + + +def test_idempotent_function_with_datetime(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"order_id": "ord-001", "created_at": "2024-03-20T14:30:00"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_datetime..process_order#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="order", + persistence_store=persistence_layer, + config=config, + ) + def process_order(order: OrderWithDatetime) -> dict: + return {"status": "ok"} + + # WHEN + order = OrderWithDatetime(order_id="ord-001", created_at=datetime(2024, 3, 20, 14, 30, 0)) + result = process_order(order=order) + + # THEN + assert result == {"status": "ok"} + + +def test_idempotent_function_with_decimal(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"invoice_id": "inv-001", "amount": "199.99"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_decimal..process_invoice#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="invoice", + persistence_store=persistence_layer, + config=config, + ) + def process_invoice(invoice: InvoiceWithDecimal) -> dict: + return {"status": "ok"} + + # WHEN + invoice = InvoiceWithDecimal(invoice_id="inv-001", amount=Decimal("199.99")) + result = process_invoice(invoice=invoice) + + # THEN + assert result == {"status": "ok"} + + +def test_idempotent_function_with_path(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"config_id": "cfg-001", "file_path": "/etc/app/config.yaml"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_path..process_config#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="config", + persistence_store=persistence_layer, + config=config, + ) + def process_config(config: ConfigWithPath) -> dict: + return {"status": "ok"} + + # WHEN + cfg = ConfigWithPath(config_id="cfg-001", file_path=PurePosixPath("/etc/app/config.yaml")) + result = process_config(config=cfg) + + # THEN + assert result == {"status": "ok"} diff --git a/tests/functional/idempotency/_pydantic/test_idempotency_uuid_serialization.py b/tests/functional/idempotency/_pydantic/test_idempotency_uuid_serialization.py deleted file mode 100644 index 65e8748ca06..00000000000 --- a/tests/functional/idempotency/_pydantic/test_idempotency_uuid_serialization.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -Test for issue #8065: @idempotent_function fails with UUID in Pydantic model - -Bug: _prepare_data() calls model_dump() without mode="json", which doesn't -serialize UUIDs and dates to JSON-compatible strings. -""" - -from datetime import date, datetime -from uuid import UUID, uuid4 - -import pytest -from pydantic import BaseModel - -from aws_lambda_powertools.utilities.idempotency import ( - IdempotencyConfig, - idempotent_function, -) -from aws_lambda_powertools.utilities.idempotency.base import _prepare_data -from aws_lambda_powertools.utilities.idempotency.persistence.base import ( - BasePersistenceLayer, - DataRecord, -) -from tests.functional.idempotency.utils import hash_idempotency_key - - -TESTS_MODULE_PREFIX = "test-func.tests.functional.idempotency._pydantic.test_idempotency_uuid_serialization" - - -class MockPersistenceLayer(BasePersistenceLayer): - """Mock persistence layer that tracks idempotency key assertions.""" - - def __init__(self, expected_idempotency_key: str): - self.expected_idempotency_key = expected_idempotency_key - super().__init__() - - def _put_record(self, data_record: DataRecord) -> None: - assert data_record.idempotency_key == self.expected_idempotency_key - - def _update_record(self, data_record: DataRecord) -> None: - assert data_record.idempotency_key == self.expected_idempotency_key - - def _get_record(self, idempotency_key) -> DataRecord: - ... - - def _delete_record(self, data_record: DataRecord) -> None: - ... - - -class PaymentWithUUID(BaseModel): - """Pydantic model with UUID field - reproduces issue #8065.""" - - payment_id: UUID - customer_id: str - - -class EventWithDate(BaseModel): - """Pydantic model with date field.""" - - event_id: str - event_date: date - - -class OrderWithDatetime(BaseModel): - """Pydantic model with datetime field.""" - - order_id: str - created_at: datetime - - -def test_prepare_data_pydantic_with_uuid(): - """ - Test that _prepare_data correctly serializes Pydantic models with UUID fields. - - Issue #8065: model_dump() without mode="json" returns UUID objects instead of strings, - which causes "Object of type UUID is not JSON serializable" error. - """ - # GIVEN a Pydantic model with UUID - payment_uuid = uuid4() - payment = PaymentWithUUID(payment_id=payment_uuid, customer_id="customer-123") - - # WHEN preparing data for idempotency - result = _prepare_data(payment) - - # THEN UUID should be serialized as string (not UUID object) - assert isinstance(result, dict) - assert isinstance(result["payment_id"], str), ( - f"UUID should be serialized as string, got {type(result['payment_id'])}" - ) - assert result["payment_id"] == str(payment_uuid) - assert result["customer_id"] == "customer-123" - - -def test_prepare_data_pydantic_with_date(): - """Test that _prepare_data correctly serializes Pydantic models with date fields.""" - # GIVEN a Pydantic model with date - event_date = date(2024, 1, 15) - event = EventWithDate(event_id="event-123", event_date=event_date) - - # WHEN preparing data for idempotency - result = _prepare_data(event) - - # THEN date should be serialized as ISO format string - assert isinstance(result, dict) - assert isinstance(result["event_date"], str), ( - f"date should be serialized as string, got {type(result['event_date'])}" - ) - assert result["event_date"] == "2024-01-15" - - -def test_prepare_data_pydantic_with_datetime(): - """Test that _prepare_data correctly serializes Pydantic models with datetime fields.""" - # GIVEN a Pydantic model with datetime - created_at = datetime(2024, 1, 15, 10, 30, 0) - order = OrderWithDatetime(order_id="order-123", created_at=created_at) - - # WHEN preparing data for idempotency - result = _prepare_data(order) - - # THEN datetime should be serialized as ISO format string - assert isinstance(result, dict) - assert isinstance(result["created_at"], str), ( - f"datetime should be serialized as string, got {type(result['created_at'])}" - ) - - -def test_idempotent_function_with_uuid_in_pydantic_model(): - """ - Integration test for idempotent_function with UUID in Pydantic model. - - This is the main test case for issue #8065. - """ - # GIVEN - config = IdempotencyConfig(use_local_cache=True) - payment_uuid = UUID("12345678-1234-5678-1234-567812345678") - mock_event = {"payment_id": str(payment_uuid), "customer_id": "customer-456"} - - idempotency_key = ( - f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_uuid_in_pydantic_model" - f"..process_payment#{hash_idempotency_key(mock_event)}" - ) - persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) - - @idempotent_function( - data_keyword_argument="payment", - persistence_store=persistence_layer, - config=config, - ) - def process_payment(payment: PaymentWithUUID) -> dict: - return {"status": "processed", "payment_id": str(payment.payment_id)} - - # WHEN processing payment with UUID - payment = PaymentWithUUID(payment_id=payment_uuid, customer_id="customer-456") - - # THEN it should not raise "Object of type UUID is not JSON serializable" - result = process_payment(payment=payment) - assert result["status"] == "processed" - assert result["payment_id"] == str(payment_uuid)