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
- Dates and times: pass
default=strfor a quick fix, ordefault=lambda o: o.isoformat()to keep ISO 8601 format. - Sets: convert at the source,
list(my_set), since set order isn't guaranteed anyway. - Decimal:
default=strpreserves precision as text;default=floatloses precision but stays numeric. - Dataclasses and Pydantic models: use
dataclasses.asdict()or the model's own.model_dump()/.dict()before callingjson.dumps(), rather than writing adefault=for every field type. - Can't find which field broke in a big object: write a one-off recursive walker that calls
json.dumps()on each value and prints the first key path that raises, since the exception itself won't tell you.
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.