json.JSONEncoder

Override one method, default(o), and every json.dumps(..., cls=YourEncoder) call knows your types. Return something serializable, or call super().default(o) to get the standard TypeError.

json classPython 2.6+Live demo
Common call
json.dumps(data, cls=MyEncoder)
Returns
encode() → str; iterencode() → iterator of str chunks
Replaces
a default= function passed to every dumps call
Watch out
default() is only called for types json cannot already encode
json.JSONEncoder(*, skipkeysskipkeys — Skip dict keys that are not str, int, float, bool or None.type: bool · default: False=False, ensure_asciiensure_ascii — Escape non-ASCII characters as \uXXXX.type: bool · default: True=True, check_circularcheck_circular — Detect self-referencing containers (ValueError).type: bool · default: True=True, allow_nanallow_nan — Emit NaN / Infinity; False raises ValueError.type: bool · default: True=True, sort_keyssort_keys — Emit dict keys in sorted order.type: bool · default: False=False, indentindent — Pretty-print with this indent per level. Also switches the default item separator from ", " to ",".type: int | str | None · default: None=None, separatorsseparators — Sets item_separator and key_separator.type: tuple[str, str] · default: None=None, defaultdefault — If given, replaces the default() method on this instance.type: callable · default: None=None)
→ JSONEncoder

Demo

Live evaluation
The standard recipe: handle your types in default(), hand everything else to super().default(o).
Try:
Inputs
tagslist[str]comma-separated → a set
Code
import json
from datetime import date
class Encoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, set):
            return sorted(o)
        if isinstance(o, date):
            return o.isoformat()
        return super().default(o)
json.dumps({'tags': set(['dev', 'admin', 'dev']), 'day': date(2026, 9, 29)}, cls=Encoder)
Result
'{"tags": ["admin", "dev"], "day": "2026-09-29"}'

In the subclass tab json calls default() twice — once for the set, once for the date — and encodes whatever comes back; the set becomes a sorted list, so duplicates are gone and the order is stable. In iterencode(), keys, ": " and ", " between dict items are separate pieces, while list items carry the "[" or ", " in front of them. With any indent other than None, item_separator is "," — the line break and indentation take the place of the space.

Parameters

NameTypeRequiredDescription
defaultcallableno (None)If given, replaces the default() method on this instance.
indentint | str | Noneno (None)Pretty-print with this indent per level. Also switches the default item separator from ", " to ",".
separatorstuple[str, str]no (None)Sets item_separator and key_separator.
sort_keysboolno (False)Emit dict keys in sorted order.
ensure_asciiboolno (True)Escape non-ASCII characters as \uXXXX.
allow_nanboolno (True)Emit NaN / Infinity; False raises ValueError.
skipkeysboolno (False)Skip dict keys that are not str, int, float, bool or None.
check_circularboolno (True)Detect self-referencing containers (ValueError).

Return value

JSONEncoder — An encoder; call .encode(obj) for a str or .iterencode(obj) for the pieces.

Common patterns

One encoder for the whole project
Collect every custom type in one default(); pass cls= wherever you serialize.
import json
from datetime import date, datetime
from decimal import Decimal
from enum import Enum
from uuid import UUID

class AppEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, (date, datetime)):
            return o.isoformat()
        if isinstance(o, (Decimal, UUID)):
            return str(o)
        if isinstance(o, (set, frozenset)):
            return sorted(o)
        if isinstance(o, Enum):
            return o.value
        return super().default(o)

json.dumps(payload, cls=AppEncoder)
Dataclasses
dataclasses.asdict turns a dataclass (recursively) into a dict json understands.
import dataclasses, json

class DataclassEncoder(json.JSONEncoder):
    def default(self, o):
        if dataclasses.is_dataclass(o) and not isinstance(o, type):
            return dataclasses.asdict(o)
        return super().default(o)
Stream a large document
Write the chunks as they come instead of building one huge string.
import json
for chunk in json.JSONEncoder().iterencode(big_data):
    sock.sendall(chunk.encode('utf-8'))

Examples

1. encode() returns the str
import json json.JSONEncoder(sort_keys=True).encode({'b': 1, 'a': 2})
Returns
'{"a": 2, "b": 1}'
2. iterencode() yields pieces
import json list(json.JSONEncoder().iterencode({'a': [1, 2]}))
Returns
['{', '"a"', ': ', '[1', ', 2', ']', '}']
3. A subclass via cls=
import json class Encoder(json.JSONEncoder): def default(self, o): if isinstance(o, set): return sorted(o) return super().default(o) json.dumps({'tags': {'b', 'a'}}, cls=Encoder)
Returns
'{"tags": ["a", "b"]}'
4. Unhandled types still raise
import json class Encoder(json.JSONEncoder): def default(self, o): if isinstance(o, set): return sorted(o) return super().default(o) json.dumps({'tags': frozenset({'a'})}, cls=Encoder)
Returns
TypeError: Object of type frozenset is not JSON serializable
5. The base default() always raises
import json json.JSONEncoder().default(1)
Returns
TypeError: Object of type int is not JSON serializable
6. default= replaces the method
import json json.JSONEncoder(default=str).default
Returns
<class 'str'>
7. Separators follow indent
import json enc = json.JSONEncoder(indent=2) (enc.item_separator, enc.key_separator)
Returns
(',', ': ')
8. Streaming into a file object
import io, json out = io.StringIO() for chunk in json.JSONEncoder().iterencode(list(range(5))): out.write(chunk) out.getvalue()
Returns
'[0, 1, 2, 3, 4]'

Pitfalls

1. Forgetting to return from default()
A default() that falls off the end returns None — and None is perfectly serializable, so the value silently becomes null.
no return
import json
class Encoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, set):
            sorted(o)
json.dumps({'tags': {'a'}}, cls=Encoder)
'{"tags": null}'
return + super
import json
class Encoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, set):
            return sorted(o)
        return super().default(o)
json.dumps({'tags': {'a'}}, cls=Encoder)
'{"tags": ["a"]}'
2. Trying to change how floats (or str, int, list…) are written
default() is only consulted for objects json cannot encode on its own. A float never reaches it, so rounding there does nothing — transform the data before dumping.
default() for float
import json
class Rounder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, float):
            return round(o, 2)
        return super().default(o)
json.dumps({'pi': 3.14159}, cls=Rounder)
'{"pi": 3.14159}'
round first
import json
data = {'pi': 3.14159}
json.dumps({k: round(v, 2) for k, v in data.items()})
'{"pi": 3.14}'
3. Overriding encode() — json.dump never calls it
dumps goes through encode(), but dump goes straight to iterencode(). Customize default() (or the data), not encode().
encode() override
import io, json
class Upper(json.JSONEncoder):
    def encode(self, o):
        return super().encode(o).upper()
buf = io.StringIO()
json.dump({'a': 'x'}, buf, cls=Upper)
(json.dumps({'a': 'x'}, cls=Upper), buf.getvalue())
('{"A": "X"}', '{"a": "x"}')
dumps, then write
import io, json
class Upper(json.JSONEncoder):
    def encode(self, o):
        return super().encode(o).upper()
buf = io.StringIO()
buf.write(json.dumps({'a': 'x'}, cls=Upper))
buf.getvalue()
'{"A": "X"}'

When to use

Use it
  • The same custom types (dates, Decimal, UUID, dataclasses) are serialized in many places
  • Streaming output chunk by chunk with iterencode()
  • Frameworks that accept an encoder class (cls=, json_encoder=)
Reach for something else
  • A one-off conversion → json.dumps(data, default=func)
  • Changing how floats, strings or lists are written → transform the data first
  • Parsing → JSONDecoder / json.loads

Notes

CPython impl
Lib/json/encoder.py; encode() uses the C encoder from Modules/_json.c in one shot, iterencode() yields from the pure-Python _make_iterencode
Hook order
default(o) is tried only after str, int, float, bool, None, list, tuple and dict; its return value is encoded again (and may itself go through default)
Circular
check_circular also covers default(): returning the object itself raises ValueError: Circular reference detected
Attributes
skipkeys, ensure_ascii, check_circular, allow_nan, sort_keys and indent are set on every instance; item_separator (', ') and key_separator (': ') are class attributes, overridden on the instance by separators= — or item_separator alone by indent=

FAQ

Subclass json.JSONEncoder, override default(self, o) to return a serializable value for your types, end it with return super().default(o), and pass the class as json.dumps(data, cls=YourEncoder).