Repository navigation
Description
Expected Behaviour
When @idempotent_function uses output_serializer=PydanticSerializer and the function returns a Pydantic model with UUID, datetime, date, plain Enum, bytes or set fields, the result should be saved as a COMPLETED record. Later calls with the same payload should get the model back without running the function again.
The same model already works elsewhere:
- As an input:
_prepare_datadumps Pydantic models in JSON mode since fix(idempotency): serialize Pydantic models with mode='json' for UUID/date support #8075. - As a result with no
output_serializer: it's stored, but replayed as a dict.
It only fails with the serializer that's meant to give the model back.
Current Behaviour
PydanticSerializer.to_dict returns model_dump(), which is Python mode, so the dict still holds UUID, datetime and Enum objects. save_success then runs json.dumps(result, cls=Encoder, sort_keys=True). The Encoder only handles Decimal, Pydantic models and dataclasses, so it raises TypeError, and the decorator raises:
IdempotencyPersistenceLayerError: Failed to update record state to success in idempotency store - (Object of type datetime is not JSON serializable)
By then the function has already run, and the record stays INPROGRESS:
- Retry before the in-progress expiry: it gets
IdempotencyAlreadyInProgressError. - Retry after it (e.g. an async invocation retry or an SQS redelivery): the function runs again and fails the same way.
So a payment-style side effect can run once per retry. In a local run with a fake persistence layer and three retries after the in-progress window, the side effect ran 3 times.
Code snippet
import os
from datetime import datetime, timezone
from enum import Enum
from uuid import UUID, uuid4
from pydantic import BaseModel
from aws_lambda_powertools.utilities.idempotency import (
DynamoDBPersistenceLayer,
IdempotencyConfig,
idempotent_function,
)
from aws_lambda_powertools.utilities.idempotency.serialization.pydantic import PydanticSerializer
from aws_lambda_powertools.utilities.typing import LambdaContext
persistence_layer = DynamoDBPersistenceLayer(table_name=os.environ["IDEMPOTENCY_TABLE"])
config = IdempotencyConfig(event_key_jmespath="order_id")
class PaymentStatus(Enum):
CHARGED = "charged"
class Order(BaseModel):
order_id: str
amount: int
class Payment(BaseModel):
payment_id: UUID
charged_at: datetime
status: PaymentStatus
@idempotent_function(
data_keyword_argument="order",
config=config,
persistence_store=persistence_layer,
output_serializer=PydanticSerializer,
)
def charge(order: Order) -> Payment:
# call the payment provider here
return Payment(payment_id=uuid4(), charged_at=datetime.now(timezone.utc), status=PaymentStatus.CHARGED)
def lambda_handler(event: dict, context: LambdaContext):
config.register_lambda_context(context)
payment = charge(order=Order(**event))
return payment.model_dump(mode="json")Possible Solution
Dump in JSON mode, as _prepare_data and shared.functions.pydantic_to_dict already do:
def to_dict(self, data: BaseModel) -> dict:
return data.model_dump(mode="json")from_dict uses model_validate, which parses the stored strings back into UUID, datetime, Enum and so on, so it doesn't need to change. For models that already work, the stored JSON stays the same. I have a fix with tests and will open a PR for it.
DataclassSerializer has the same gap, since asdict() keeps the raw objects. Its from_dict (Model(**data)) can't turn strings back into those types, though, so it needs its own fix.
Steps to Reproduce
The root cause, locally and without AWS:
import json
from datetime import datetime, timezone
from uuid import UUID, uuid4
from pydantic import BaseModel
from aws_lambda_powertools.shared.json_encoder import Encoder
from aws_lambda_powertools.utilities.idempotency.serialization.pydantic import PydanticSerializer
class Payment(BaseModel):
payment_id: UUID
charged_at: datetime
payment = Payment(payment_id=uuid4(), charged_at=datetime.now(timezone.utc))
json.dumps(PydanticSerializer(Payment).to_dict(payment), cls=Encoder, sort_keys=True) # what save_success does
# TypeError: Object of type datetime is not JSON serializableWith the decorator, as in the snippet above, using any persistence layer:
- Call
charge(order=...). The function body runs, thenIdempotencyPersistenceLayerErroris raised, and the record staysINPROGRESS. - Call it again with the same order after the in-progress expiry. The function body runs again.
Powertools for AWS Lambda (Python) version
latest (3.36.0); every release since PydanticSerializer was added in v2.24.0 (#2951)
AWS Lambda function runtime
3.14 (reproduced locally on Python 3.14.4, Pydantic 2.13.5; not runtime-specific)
Packaging format used
PyPi
Debugging logs
Traceback (most recent call last):
File ".../aws_lambda_powertools/utilities/idempotency/base.py", line 300, in _get_function_response
self.persistence_store.save_success(data=self.data, result=serialized_response)
File ".../aws_lambda_powertools/utilities/idempotency/persistence/base.py", line 302, in save_success
response_data = json.dumps(result, cls=Encoder, sort_keys=True)
...
TypeError: Object of type datetime is not JSON serializable
when serializing dict item 'charged_at'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
File ".../aws_lambda_powertools/utilities/idempotency/base.py", line 302, in _get_function_response
raise IdempotencyPersistenceLayerError(
aws_lambda_powertools.utilities.idempotency.exceptions.IdempotencyPersistenceLayerError: Failed to update record state to success in idempotency store - (Object of type datetime is not JSON serializable)Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsComing soon
Activity
powertools-for-aws-oss-automation commentedon Oct 17, 2026
Warning
This issue is now closed. Please be mindful that future comments are hard for our team to see.
If you need more assistance, please either reopen the issue, or open a new issue referencing this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.
github-actions commentedon Oct 22, 2026
This is now released under 3.37.0 version!