Unable to marshal response: Object of type Decimal is not JSON serializable

Short answer

Your handler ran fine and returned successfully — then the runtime failed serialising the return value to JSON. The work is done and the result is lost, which is why this is so much more annoying than an ordinary exception.

What it means

A Lambda handler does not return a value to a caller directly. It returns it to the runtime, which serialises it to JSON and hands the bytes back to the invoker. That serialisation step is invisible until it fails, and when it does the failure lands after every piece of work your function did.

This is what makes the error feel out of order. Your logs show the request arriving, the database being read, the result being assembled, and a final line saying the work is complete. Then the invocation fails. Nothing your code did went wrong — the value it produced simply could not be expressed in JSON, and the result is discarded along with everything it took to compute.

Almost every Python instance traces back to decimal.Decimal. Boto3's DynamoDB resource deserialises every numeric attribute as Decimal rather than int or float, deliberately, because floats cannot represent decimal fractions exactly and silently corrupting a monetary value would be worse than an inconvenient type. The consequence is that any item returned straight from DynamoDB is full of objects json.dumps refuses, often nested inside lists and maps where they are invisible in a quick read of the code.

The reason it survives local testing so reliably is that print() and logging call str(), which every one of these types implements perfectly. A Decimal prints as 4. A datetime prints as a readable timestamp. So a developer logs the object, sees exactly what they expected, returns it, and the runtime rejects it — the same value passing one representation and failing another. Being able to read the successful log lines and the marshal failure as one invocation is what makes that sequence obvious rather than baffling.

Counts

CloudWatch’s Errors metric: The error escaped your handler, so Lambda reports the invocation as failed and CloudWatch’s Errors metric counts it. LogStitch classified the invocation above as uncaught.

What it looks like in CloudWatch

This is the shape the failure arrives in: the lines of one invocation scattered among everything else the log group received at the same moment.

CloudWatch Logs Before

6 raw lines, in the order CloudWatch delivered them.

2026-08-30T14:07:33.114Z START RequestId: 8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92 Version: $LATEST
2026-08-30T14:07:33.118Z 8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92 INFO Fetching order ord_92ba7c
2026-08-30T14:07:33.244Z 8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92 INFO Order loaded items=3 total=4288
2026-08-30T14:07:33.288Z 8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92 [ERROR] Runtime.MarshalError: Unable to marshal response: Object of type Decimal is not JSON serializable
2026-08-30T14:07:33.402Z END RequestId: 8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92
2026-08-30T14:07:33.402Z REPORT RequestId: 8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92 Duration: 288.44 ms Billed Duration: 289 ms Memory Size: 512 MB Max Memory Used: 96 MB Status: error

LogStitch After

The same lines, grouped into the invocation they belong to.

Error8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92dur 288msbilled 289msmem 96/512MBlogs 6
  1. 14:07:33.114PLAT
    START RequestId: 8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92 Version: $LATEST
  2. 14:07:33.118INFO
    INFO	Fetching order ord_92ba7c
  3. 14:07:33.244INFO
    INFO	Order loaded items=3 total=4288
  4. 14:07:33.288ERROR
    [ERROR] Runtime.MarshalError: Unable to marshal response: Object of type Decimal is not JSON serializable
  5. 14:07:33.402PLAT
    END RequestId: 8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92
  6. 14:07:33.402PLAT
    REPORT RequestId: 8a2f6c04-7b19-4e35-90d8-1c4b7e5a3f92	Duration: 288.44 ms	Billed Duration: 289 ms	Memory Size: 512 MB	Max Memory Used: 96 MB	Status: error
Error
Status
288ms
Duration
289ms
Billed
96/512MB
Memory
81%
Headroom
No
Cold start
Runtime.MarshalError
Error type

The panel on the right is generated by running the excerpt on the left through the same parser that powers the free web stitcher — it is what the tool actually produces for this input, not an illustration of it.

How to confirm it from the logs

Position is the giveaway. This appears after all of your own logging, including whatever you logged just before returning, because it happens when the runtime marshals the value your handler already produced. If your logs show the work completing and then a failure, this is the shape.

The type named in the message tells you what to fix, and Decimal is by far the most common because that is what boto3 returns for every DynamoDB number. datetime is second. Both are ordinary Python objects with no JSON equivalent, and neither is anything you wrote.

Causes, most likely first

1

The value came from DynamoDB and contains Decimal

How to confirm

Check whether the returned object originated from a get_item, query or scan. Boto3 deserialises every DynamoDB number as decimal.Decimal to avoid float precision loss, so any item returned straight from the table carries them — including nested inside lists and maps where they are easy to miss.

2

A datetime, date, or UUID is in the response

How to confirm

Look for timestamp or identifier fields in the returned structure. datetime, date, UUID and bytes all serialise fine in your own logs via str() and all fail json.dumps, so printing the object successfully proves nothing about whether it can be returned.

3

A set is being returned where a list is needed

How to confirm

Search for set() or a set comprehension in the response path. Sets have no JSON representation at all, and DynamoDB string sets deserialise into Python sets, so this often arrives from the same place the Decimals do.

4

A whole SDK response object is being returned

How to confirm

Check whether the handler returns the raw result of a boto3 call rather than a field from it. Those responses carry ResponseMetadata, streaming bodies and datetime values, none of which belong in an API response even when they can be serialised.

Fixes

Fix 1

Give json.dumps a default for the types it does not know

Serialise deliberately in the handler rather than letting the runtime do it implicitly. A default function handles the types Python has and JSON does not, and — importantly — it converts Decimal to int when the value is whole, so identifiers and counts do not come back as 4.0.

pythonimport json
from decimal import Decimal
from datetime import date, datetime
from uuid import UUID

def encode(value):
    if isinstance(value, Decimal):
        # Whole numbers should not become 4.0 in the response.
        return int(value) if value % 1 == 0 else float(value)
    if isinstance(value, (datetime, date)):
        return value.isoformat()
    if isinstance(value, (set, frozenset)):
        return sorted(value)
    if isinstance(value, UUID):
        return str(value)
    if isinstance(value, bytes):
        return value.decode()
    raise TypeError(f"Cannot serialise {type(value).__name__}")

def lambda_handler(event, context):
    item = table.get_item(Key={"id": event["id"]})["Item"]
    return {
        "statusCode": 200,
        "body": json.dumps(item, default=encode),
    }
Fix 2

Convert at the boundary rather than at the end

Normalising the item as soon as it leaves DynamoDB means the rest of the function works with ordinary Python types, and nothing downstream has to remember that numbers are secretly Decimals.

pythonfrom decimal import Decimal

def normalise(value):
    if isinstance(value, list):
        return [normalise(item) for item in value]
    if isinstance(value, dict):
        return {key: normalise(item) for key, item in value.items()}
    if isinstance(value, Decimal):
        return int(value) if value % 1 == 0 else float(value)
    return value

item = normalise(table.get_item(Key=key)["Item"])
Fix 3

Return a shape you built, not one a client handed you

Constructing the response explicitly — naming the fields you intend to return — avoids the whole class of problem and stops SDK metadata leaking into your API. It is also the only version that keeps working when the upstream response shape changes.

Also seen as

The same underlying failure, worded differently by a different runtime, SDK version, or logging layer. All of these land here — there is no separate page for each phrasing.

Unable to marshal response: Object of type datetime is not JSON serializableTypeError: Object of type set is not JSON serializableRuntime.MarshalErrorlambda decimal not json serializablelambda datetime not json serializabledynamodb decimal lambda

Errors that show up alongside this one, or that people mistake for it.

References

LogStitch finds this automatically, across every invocation in your account.

Paste a log excerpt into the free web stitcher and see it grouped, classified, and measured in your browser — nothing is uploaded. Or run the Mac app against your own AWS profiles and get the same view over every function you own.