TypeError: Object of type X Is Not JSON Serializable (tested)

json.dumps() only knows how to write a handful of Python types. Everything else raises TypeError: Object of type X is not JSON serializable — and the message never tells you which key in your dict broke. We fed it the common offenders and measured exactly what happens.

1. The error, for every common offender

import json, datetime, decimal

json.dumps(datetime.datetime(2024, 1, 1))
TypeError: Object of type datetime is not JSON serializable

json.dumps(datetime.date(2024, 1, 1))
TypeError: Object of type date is not JSON serializable

json.dumps({1, 2, 3})
TypeError: Object of type set is not JSON serializable

json.dumps(b"hello")
TypeError: Object of type bytes is not JSON serializable

json.dumps(decimal.Decimal("1.5"))
TypeError: Object of type Decimal is not JSON serializable

json.dumps(1 + 2j)
TypeError: Object of type complex is not JSON serializable

Measured: json.dumps() natively handles exactly dict, list, tuple, str, int, float, bool, and None. Every other type raises this same TypeError, naming only the type — not the value, not which field it came from.

2. The message does not tell you which key broke

json.dumps({"a": 1, "b": {"c": datetime.datetime(2024, 1, 1)}})
TypeError: Object of type datetime is not JSON serializable

Measured: nesting the bad value three levels deep produced the exact same message as dumping it directly — no key path, no indication it was under "b" > "c". On a large real-world dict this is the actual debugging problem: you know the type, not the location. The fastest way to find it is a small recursive walk that applies json.dumps() to each leaf value and reports the first one that fails, rather than guessing from the type alone.

3. The fix: a default= callback

def default(o):
    if isinstance(o, datetime.datetime):
        return o.isoformat()
    raise TypeError(f"not serializable: {type(o)}")

json.dumps({"a": datetime.datetime(2024, 1, 1)}, default=default)
'{"a": "2024-01-01T00:00:00"}'

Measured: default= is called once per unhandled object and whatever it returns is serialized in place. It is not recursive magic — if it returns another non-serializable object, that object goes through the normal rules next (a dict or a JSON-safe type works; another datetime would need handling too). Returning str(o) as a catch-all default is the fastest blanket fix when you don't care about round-tripping the type back.

4. A surprise: dict keys don't need to be strings

json.dumps({1: "a", 2: "b"})
'{"1": "a", "2": "b"}'

json.dumps({(1, 2): "a"})
TypeError: keys must be str, int, float, bool or None, not tuple

Measured: int, float, bool and None keys are silently coerced to their string form — no error, no warning, and True becomes the string "true" while None becomes "null". Only a genuinely unsupported key type, like a tuple, raises — and that error message is worded differently from the value-side one above (keys must be str, int, float, bool or None, not X vs. Object of type X is not JSON serializable), which is a useful tell for whether the broken value is a key or a value.

5. NaN and Infinity serialize by default — and that JSON is invalid

json.dumps(float("nan"))
'NaN'
json.dumps(float("inf"))
'Infinity'

json.dumps(float("nan"), allow_nan=False)
ValueError: Out of range float values are not JSON compliant: nan

Measured: by default json.dumps() happily writes NaN and Infinity as bare, unquoted tokens — valid Python-ish output, but not valid per the JSON spec (RFC 8259), which has no representation for non-finite numbers. A strict parser in another language will reject that output. Pass allow_nan=False to catch these before they leak into a response, which raises a ValueError, not the TypeError the type errors above raise.

6. Fast fixes, in order of how often we'd reach for them

Frequently asked questions

Why does json.dumps() say "Object of type datetime is not JSON serializable" instead of naming the value?

We confirmed the message reports only the Python type, never the value or which dict key it came from, even three levels deep in a nested structure — you have to locate the field yourself.

Does json.dumps() error on integer or boolean dict keys?

No — we measured that int, float, bool and None keys are silently converted to their string form (1 becomes "1", True becomes "true") with no error; only a genuinely unsupported key type like a tuple raises, and with a differently worded message than the value-side error.

What does the default= argument to json.dumps() actually do?

We confirmed it's called once per object json can't natively serialize, and whatever it returns is serialized in its place — it isn't automatically recursive, so if it returns another non-serializable object, that one needs handling too.

Does json.dumps() reject NaN and Infinity?

Not by default — we measured that json.dumps(float('nan')) returns the bare token NaN, which is not valid per the JSON spec and can break strict parsers in other languages; passing allow_nan=False makes it raise a ValueError instead.

What's the fastest fix for a dataclass or Pydantic model that won't serialize?

Convert it first — we'd reach for dataclasses.asdict() or the model's own .model_dump()/.dict() method before calling json.dumps(), rather than writing a default= callback that handles every field type by hand.

Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in json module, calling json.dumps() directly on each value and printing the exact exception text. Every message below is copy-pasted output, not paraphrased. PlainHeaders is our extension; Python's json module is built in.