sethserver.com Subscribe
A hand points to a laptop screen displaying "[X]" in red text with a dollar sign icon nearby, set against a vibrant geometric background with shapes in blue, pink, orange, yellow, and purple, plus a clock icon on the left.

Fix "Object of type X is not JSON serializable" in Python

By Seth Black • Updated: September 17, 2026

Python · 4 min read

Like this kind of writing? Get one email a week with notes on startups, AI, and the occasional strong opinion about Python: subscribe to the newsletter.

It usually happens right after a deploy. Everything worked in dev. Then one customer shows up with one weird record, and your API starts throwing 500s because a Decimal took an unexpected code path.

You hit an endpoint that worked fine all week, and suddenly you get:

TypeError: Object of type Decimal is not JSON serializable

Python isn't being dramatic. It's being literal. Computers are literal and petty. JSON has a tiny set of types it supports, and decimal.Decimal, datetime, UUID, Enum, and your nice tidy @dataclass are not on the list.

Why this error exists (and why it's not dumb)

JSON supports:

That's it. No money type. No timestamp type. No "this is clearly an ID" type.

Python's json module refuses to guess conversions because guessing creates bugs that hide until they get expensive. If Python silently turned Decimal("99.99") into a float, you'd ship rounding errors into billing. That's a real problem.

The quick fix that will hurt you later

People do this:

json.dumps(data, default=lambda obj: f"{obj}")

Yes, it stops the crash. It also turns your API into a grab bag of "whatever string happened to come out."

Concrete ways this breaks:

The crash isn't the real issue. The real issue is pretending JSON is "just strings" until a customer teaches your system a new way to break.

The fix: pick encoding rules once, and use them everywhere

You want a single encoder function that makes explicit choices. Not perfect choices. Consistent choices.

I like dataclasses because they reduce mystery-meat objects and because I'm tired and I enjoy having nice things. Here's how I handle the common types:

import json
import decimal
import datetime
import uuid
from enum import Enum
from dataclasses import is_dataclass, asdict

def api_json_default(obj):
    if isinstance(obj, decimal.Decimal):
        return str(obj)

    if isinstance(obj, datetime.datetime):
        return obj.isoformat()

    if isinstance(obj, datetime.date):
        return obj.isoformat()

    if isinstance(obj, uuid.UUID):
        return str(obj)

    if isinstance(obj, Enum):
        return obj.value

    if is_dataclass(obj) and not isinstance(obj, type):
        return asdict(obj)

    raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")

Use it like this:

payload = json.dumps(data, default=api_json_default)

Now the weird record doesn't crash production. Clients get consistent data.

Nested dataclasses: the nice part

asdict() is recursive. So if you have an Order dataclass that contains LineItem dataclasses, it handles the nesting. Any non-JSON types inside still flow through default= when json hits them.

Timezones: the footgun you already stepped on

Naive datetimes (no timezone info) are a footgun. .isoformat() will serialize them, but you're shipping ambiguity.

Your API has to decide:

Pick one and enforce it. Add a test. Put it in the code review checklist. This is boring work that saves you later.

If you're working with timestamps across systems or users in different time zones, storing everything in UTC and only converting at display time prevents DST bugs and scheduling chaos.

The "nuclear option": a custom JSONEncoder

You can also subclass json.JSONEncoder:

class CustomJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        return api_json_default(obj)

json.dumps(data, cls=CustomJSONEncoder)

I call this the nuclear option because it's global and powerful. It can become a shared utility everyone is afraid to edit. Use it if you need it. Prefer default= when you can keep it local and obvious.

Final thought

Computers are literal and petty. Humans are flexible and emotional.

Define explicit, consistent encoding rules for non-JSON Python types once. Implement them everywhere using the default= parameter. Don't use string conversion shortcuts that create ambiguous data structures for API consumers.

If you want to eliminate this kind of serialization headache entirely, consider using Pydantic to handle validation and JSON parsing with built-in type coercion and clear error messages.

Reference: Python docs on encoders/decoders: https://docs.python.org/3/library/json.html#encoders-and-decoders

-Sethers

Share this post
Newsletter

One email, once a week.

Notes on databases, systems, and the occasional strong opinion about Python. No spam, unsubscribe anytime.

Seth Black
Written by

Seth Black

Engineer and founder based in Texas. Writes about databases, AI, and running things in production. Embeds in small teams as lead engineer.

More from Python

View all →
Python

OpenAI Bought Astral - and my fav tool uv

Mar 23, 2026
Python

Python Pydantic Validation: Stop Writing Manual Checks

Mar 04, 2026
Python

Python asyncio Recipes

Mar 04, 2026