Skip to content

Commit 1141db1

Browse files
committed
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
1 parent c4434b7 commit 1141db1

File tree

2 files changed

+158
-1
lines changed

2 files changed

+158
-1
lines changed

aws_lambda_powertools/utilities/idempotency/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def _prepare_data(data: Any) -> Any:
6060

6161
# Convert from Pydantic model
6262
if callable(getattr(data, "model_dump", None)):
63-
return data.model_dump()
63+
return data.model_dump(mode="json")
6464

6565
# Convert from event source data class
6666
if callable(getattr(data, "dict", None)):
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""
2+
Test for issue #8065: @idempotent_function fails with UUID in Pydantic model
3+
4+
Bug: _prepare_data() calls model_dump() without mode="json", which doesn't
5+
serialize UUIDs and dates to JSON-compatible strings.
6+
"""
7+
8+
from datetime import date, datetime
9+
from uuid import UUID, uuid4
10+
11+
import pytest
12+
from pydantic import BaseModel
13+
14+
from aws_lambda_powertools.utilities.idempotency import (
15+
IdempotencyConfig,
16+
idempotent_function,
17+
)
18+
from aws_lambda_powertools.utilities.idempotency.base import _prepare_data
19+
from aws_lambda_powertools.utilities.idempotency.persistence.base import (
20+
BasePersistenceLayer,
21+
DataRecord,
22+
)
23+
from tests.functional.idempotency.utils import hash_idempotency_key
24+
25+
26+
TESTS_MODULE_PREFIX = "test-func.tests.functional.idempotency._pydantic.test_idempotency_uuid_serialization"
27+
28+
29+
class MockPersistenceLayer(BasePersistenceLayer):
30+
"""Mock persistence layer that tracks idempotency key assertions."""
31+
32+
def __init__(self, expected_idempotency_key: str):
33+
self.expected_idempotency_key = expected_idempotency_key
34+
super().__init__()
35+
36+
def _put_record(self, data_record: DataRecord) -> None:
37+
assert data_record.idempotency_key == self.expected_idempotency_key
38+
39+
def _update_record(self, data_record: DataRecord) -> None:
40+
assert data_record.idempotency_key == self.expected_idempotency_key
41+
42+
def _get_record(self, idempotency_key) -> DataRecord:
43+
...
44+
45+
def _delete_record(self, data_record: DataRecord) -> None:
46+
...
47+
48+
49+
class PaymentWithUUID(BaseModel):
50+
"""Pydantic model with UUID field - reproduces issue #8065."""
51+
52+
payment_id: UUID
53+
customer_id: str
54+
55+
56+
class EventWithDate(BaseModel):
57+
"""Pydantic model with date field."""
58+
59+
event_id: str
60+
event_date: date
61+
62+
63+
class OrderWithDatetime(BaseModel):
64+
"""Pydantic model with datetime field."""
65+
66+
order_id: str
67+
created_at: datetime
68+
69+
70+
def test_prepare_data_pydantic_with_uuid():
71+
"""
72+
Test that _prepare_data correctly serializes Pydantic models with UUID fields.
73+
74+
Issue #8065: model_dump() without mode="json" returns UUID objects instead of strings,
75+
which causes "Object of type UUID is not JSON serializable" error.
76+
"""
77+
# GIVEN a Pydantic model with UUID
78+
payment_uuid = uuid4()
79+
payment = PaymentWithUUID(payment_id=payment_uuid, customer_id="customer-123")
80+
81+
# WHEN preparing data for idempotency
82+
result = _prepare_data(payment)
83+
84+
# THEN UUID should be serialized as string (not UUID object)
85+
assert isinstance(result, dict)
86+
assert isinstance(result["payment_id"], str), (
87+
f"UUID should be serialized as string, got {type(result['payment_id'])}"
88+
)
89+
assert result["payment_id"] == str(payment_uuid)
90+
assert result["customer_id"] == "customer-123"
91+
92+
93+
def test_prepare_data_pydantic_with_date():
94+
"""Test that _prepare_data correctly serializes Pydantic models with date fields."""
95+
# GIVEN a Pydantic model with date
96+
event_date = date(2024, 1, 15)
97+
event = EventWithDate(event_id="event-123", event_date=event_date)
98+
99+
# WHEN preparing data for idempotency
100+
result = _prepare_data(event)
101+
102+
# THEN date should be serialized as ISO format string
103+
assert isinstance(result, dict)
104+
assert isinstance(result["event_date"], str), (
105+
f"date should be serialized as string, got {type(result['event_date'])}"
106+
)
107+
assert result["event_date"] == "2024-01-15"
108+
109+
110+
def test_prepare_data_pydantic_with_datetime():
111+
"""Test that _prepare_data correctly serializes Pydantic models with datetime fields."""
112+
# GIVEN a Pydantic model with datetime
113+
created_at = datetime(2024, 1, 15, 10, 30, 0)
114+
order = OrderWithDatetime(order_id="order-123", created_at=created_at)
115+
116+
# WHEN preparing data for idempotency
117+
result = _prepare_data(order)
118+
119+
# THEN datetime should be serialized as ISO format string
120+
assert isinstance(result, dict)
121+
assert isinstance(result["created_at"], str), (
122+
f"datetime should be serialized as string, got {type(result['created_at'])}"
123+
)
124+
125+
126+
def test_idempotent_function_with_uuid_in_pydantic_model():
127+
"""
128+
Integration test for idempotent_function with UUID in Pydantic model.
129+
130+
This is the main test case for issue #8065.
131+
"""
132+
# GIVEN
133+
config = IdempotencyConfig(use_local_cache=True)
134+
payment_uuid = UUID("12345678-1234-5678-1234-567812345678")
135+
mock_event = {"payment_id": str(payment_uuid), "customer_id": "customer-456"}
136+
137+
idempotency_key = (
138+
f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_uuid_in_pydantic_model"
139+
f".<locals>.process_payment#{hash_idempotency_key(mock_event)}"
140+
)
141+
persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
142+
143+
@idempotent_function(
144+
data_keyword_argument="payment",
145+
persistence_store=persistence_layer,
146+
config=config,
147+
)
148+
def process_payment(payment: PaymentWithUUID) -> dict:
149+
return {"status": "processed", "payment_id": str(payment.payment_id)}
150+
151+
# WHEN processing payment with UUID
152+
payment = PaymentWithUUID(payment_id=payment_uuid, customer_id="customer-456")
153+
154+
# THEN it should not raise "Object of type UUID is not JSON serializable"
155+
result = process_payment(payment=payment)
156+
assert result["status"] == "processed"
157+
assert result["payment_id"] == str(payment_uuid)

0 commit comments

Comments
 (0)