This PEP adds an extension mechanism to the json encoder consisting of three complementary parts, one per level of customization:
This PEP adds an extension mechanism to the json encoder
consisting of three complementary parts, one per level of
customization:
__json__() and __raw_json__();copyreg.json(cls, function) filling
copyreg.json_dispatch_table, following the precedent of
copyreg.pickle();dispatch_table
attribute on a json.JSONEncoder, mirroring
pickle.Pickler.dispatch_table.The more specific level takes precedence.
A helper class copyreg.RawJSON wraps an already-encoded JSON string
so that it is emitted verbatim, enabling representations that are not
otherwise expressible (such as serializing decimal.Decimal as a
JSON number with full precision).
Several standard library container types whose JSON form is unambiguous —
collections.deque, types.MappingProxyType,
collections.ChainMap, collections.UserDict,
collections.UserList and collections.UserString —
gain a __json__ method and therefore serialize out of the box.
The json module itself gains no new public names.
Serializing anything beyond the basic types (dict, frozendict,
list, tuple, str, int, float, bool, None)
with json.dumps() today requires either passing a default=
function to every call, or subclassing json.JSONEncoder and
threading the subclass through every call site. Both mechanisms attach
the knowledge to the call rather than to the type:
default= cannot both have it applied to one document without the
application writing a merging wrapper by hand. A library that
serializes internally (for logging, caching, IPC) cannot see the
application’s default= at all.default= must return one
of the basic types, so there is no way to emit a non-integer JSON
number that float cannot represent exactly. json.loads()
can read decimal numbers losslessly via
parse_float=decimal.Decimal, but the result cannot be written
back (python/cpython#67312). The same limitation blocks control over float
formatting (python/cpython#81022) and non-finite float representation
(python/cpython#98306, python/cpython#134717), and the general request
for emitting pre-encoded JSON (python/cpython#86957).The demand is long-standing and broad. Beyond the general proposals for
a serialization hook checked before raising TypeError
(python/cpython#71549 — open since 2016, python/cpython#79292,
python/cpython#114285, python/cpython#86931), the tracker accumulated
requests for out-of-the-box support of concrete standard library types:
decimal.Decimal (python/cpython#67312, python/cpython#118810,
python/cpython#145115), array.array (python/cpython#70451),
collections.deque (python/cpython#64973, python/cpython#73849),
types.MappingProxyType (python/cpython#79039),
collections.namedtuple() as an object (python/cpython#67661), and
datetime.datetime (python/cpython#65742); and for whole
categories: sets, frozensets, bytearrays and iterators
(python/cpython#75338), generators (python/cpython#78031), and
dictionary views (python/cpython#83377).
The idea has also been proposed independently for sixteen years, each
time reinventing part of this design: a __json__() method on
python-ideas in July 2010
and April 2020
and on discuss.python.org in 2022–2024;
raw output and a standardized encoder protocol on python-ideas in 2019;
a registration function on discuss.python.org in 2022.
Meanwhile the ecosystem adopted the convention piecemeal. TurboGears
serializes objects via a no-argument __json__() — and its
custom_encoders configuration takes precedence over it, the same
ordering as this PEP. Pyramid’s JSON renderer
calls __json__(request). Applications and libraries such as conda,
Checkmk and TatSu define __json__ today. simplejson added an opt-in for_json()
method instead — choosing that name precisely because dunder names are
reserved for the language (simplejson issue #52). In that issue’s
discussion, a GitHub code search already found more __json__
definitions in the wild than for_json ones by 2016, and formalizing
the protocol in a PEP was requested so that third-party libraries can
rely on the same signature. Only the standard library can define
__json__; this PEP does, ending the fragmentation.
The type-support requests could not simply be granted one by one. For most of the requested types the JSON representation is ambiguous:
Decimal — a JSON number (1234567890.0987654321) or a JSON
string ("1234567890.0987654321")? Both are common; each is wrong
for some consumers.namedtuple — a JSON Array (it is a tuple) or a JSON Object (it has
named fields)?array.array — a JSON Array of numbers, or a compact encoded
string? The sensible answer even depends on its type code.datetime — which of the many string representations (ISO 8601
variants, epoch seconds, RFC formats)?No standard library default can be correct, so the standard library should provide the mechanism by which each application declares its policy. That mechanism is this PEP.
Rationale One system covering all of the collected needsThe feature is deliberately a small system rather than a single hook: each part answers a different cluster of the requests above, and no part alone covers them all.
__json__ protocol answers the general extension proposals
and the third-party-type reports: the type’s author makes it
serializable once, with no imports, inherited by subclasses.Decimal,
namedtuple, array.array, datetime): the application
declares its policy in one line.__raw_json__/RawJSON) answers the requests
for representations that no basic-type conversion can express:
pre-encoded fragments, full-precision numbers, float formatting,
non-finite floats.copyreg.json(set, sorted) is a complete solution for sets, and a
hook may return any iterable or mapping-like object rather than
materializing a list or dict.__json__ methods answer the requests for
unambiguous containers (deque, mappingproxy) directly.dispatch_table scopes any of the above to one
encoder when a process needs different policies for different
outputs.The three parts correspond to the three levels at which serialization is decided, and deliberately have different shapes:
__json__() is for the author of a type. A
method needs no imports and is inherited by subclasses: a library
can make its types JSON-serializable without depending on
json or copyreg at all.copyreg.json() is for users of a type
they do not control: the application chooses, process-wide, how
third-party or ambiguous standard library types serialize.
Registration is by exact type; subclasses that want the same
treatment inherit __json__ instead.dispatch_table attribute on a
json.JSONEncoder subclass or instance customizes a
particular call, when one program needs different policies for
different outputs (an external API versus an internal cache, say).
The existing default= parameter remains the call-level catch-all
for objects no other mechanism handled.The more specific level takes precedence: a registry entry is
consulted before the type’s __json__ (the application overrides
the library), and a per-encoder table replaces the global registry
(the call overrides the application).
The registry could have been json.register(). It is placed in
copyreg instead, for three reasons.
Import cost and layering. A library that registers a serialization
function for a foreign type at import time should not pay for the
json module, whose import is dominated by re (roughly
fifty times the cost of importing copyreg, which is imported by
copy and pickle anyway). With the registry and
RawJSON both in copyreg, a registrant imports exactly one
small module. The json module imports copyreg (as
copy already does), never the reverse.
Precedent and symmetry. This is the pickle model, mirrored in both
halves: a registration function filling a global table
(copyreg.pickle() / copyreg.json()) and a per-instance
dispatch_table attribute overriding it (pickle.Pickler /
json.JSONEncoder). The function name follows the
copyreg.pickle() convention; thirty years of that precedent show
that a registration function named after its protocol causes no
confusion, because registration is a one-off, module-prefixed call.
Neutral ground. Third-party JSON encoders can honor the same
registry and helper class without importing the standard json
module, so the ecosystem can converge on a single registration point.
Placing the registry in copyreg also generalizes the module’s
charter — registering protocol implementations for types you do not
control. Registries for other protocols (such as __copy__- and
__deepcopy__-like hooks for the copy module) fit the same
pattern and will be proposed separately.
Emitting an already-encoded fragment verbatim is the only way to express
representations outside the basic types, most importantly full-precision
numbers. The chosen mechanism is a duck-typed special method,
__raw_json__(), with copyreg.RawJSON as a convenience wrapper:
__json__) that wants raw output returns
copyreg.RawJSON(fragment) — one import, which the registrant has
already paid — or an instance of its own wrapper class defining just
__raw_json__, with no imports at all.RawJSON additionally defines __json__ returning self to
serve its second function: including a pre-serialized fragment
directly in a document (as a value in a dict or list passed to
json.dumps()), not only as a hook result.The protocol, not the class, is the interface; the class is sugar.
Performance: the encoding pipeline as an invariantThe json encoder is among the hottest code in the standard
library, and the design treats its branch order as an invariant:
__json__ are consulted next — one dict
lookup plus one type-attribute lookup — before the isinstance
fallbacks, solely so that subclasses of the basic types can
customize their serialization at all (an int subclass would
otherwise be consumed by the isinstance(o, int) check, the very
problem history records for IntEnum: python/cpython#62464).isinstance fallbacks then catch non-customizing subclasses,
preserving today’s behavior for them exactly (IntEnum still
serializes as a number).__index__ as a number,
__float__ as a number, __iter__ with keys() as an
object, __iter__ alone as an array, __raw_json__ verbatim —
applies only when a hook has fired. In particular,
__raw_json__ is never looked up on an object that did not come
from a hook, so objects bound for the subclass checks or
default() pay no extra lookups.default hook runs last, unchanged.The criterion: a standard library type gets a default ``__json__``
only when its JSON form is unambiguous. Transparent containers
qualify — deque, mappingproxy, ChainMap, UserDict,
UserList, UserString are semantically just arrays and objects
(resolving python/cpython#64973, python/cpython#73849 and
python/cpython#79039 directly). Decimal, namedtuple,
array.array and datetime do not qualify, for the ambiguity
reasons given in the Motivation; for them, this PEP’s answer is a
one-line registration by the application (see How to Teach This).
This criterion is also the ready-made answer to future “just add
__json__ to X” requests.
A class may define a method __json__(self). When the json
encoder encounters an object that is not of an exactly-supported basic
type and has no dispatch-table entry, it looks up __json__ on the
object’s type (like all special methods) and, if present, calls it.
The result is then serialized in place of the object:
__index__ is serialized as a JSON number via
operator.index(); else one with __float__ as a JSON number
via float; else an iterable with a keys() method (and
__getitem__) as a JSON object; else an iterable as a JSON array;
else an object whose type defines __raw_json__ is serialized by
calling it (see below). Otherwise TypeError is raised.The result of __json__ is not recursively re-submitted to the
protocol: a result that is itself of a hook-defining type is not
converted again.
__raw_json__(self) must return a str, which the encoder
emits verbatim, without validation of its content. It is consulted
only for objects produced by a dispatch-table function or __json__
(see the pipeline invariant above), so a wrapper class used as a hook
result needs only __raw_json__. RawJSON also defines
__json__ returning self, which lets its instances appear
directly in the input document.
The following are added to copyreg:
copyreg.json(ob_type, json_function)json_function
as the serialization function for ob_type. Raises
TypeError if json_function is not callable. The
function is called with the object as its only argument and its
result is interpreted exactly like the result of __json__.
Registration is by exact type: it does not apply to subclasses of
ob_type.copyreg.json_dispatch_tablejson module (and available to other JSON encoders honoring
the registry). Applications register via copyreg.json() rather
than mutating the table directly.copyreg.RawJSON(encoded_json)encoded_json verbatim. str() of the instance returns the
fragment.Registered functions take precedence over __json__. Entries for
the exactly-supported basic types themselves (dict, list,
str, int, …) are never consulted, because the fast paths for
those types run first.
json.JSONEncoder (and the C accelerator) consult
getattr(encoder, 'dispatch_table', copyreg.json_dispatch_table), so
a subclass may set a dispatch_table class or instance attribute to
replace the global registry for that encoder, mirroring
pickle.Pickler.
Dict keys are not affected by any part of this PEP: key serialization
accepts exactly the types it accepts today, and neither the dispatch
table nor __json__ is consulted for keys (see Open Issues).
The check_circular machinery is unchanged: containers and
default results are tracked as today. Hook results are not
separately tracked; a pathological hook that returns a fresh cycle on
every call ends in RecursionError like any runaway recursion.
__json__ returning a view of the obvious container is added to:
collections.deque and types.MappingProxyType (in C,
returning self; the encoder consumes them via the array and object
paths), and collections.ChainMap,
collections.UserDict, collections.UserList,
collections.UserString (in Python, returning self or the
underlying data).
The json module gains no new public names and no existing
behavior changes for objects that define none of the new hooks:
IntEnum as a number, str subclasses as strings, dict and list
subclasses as objects and arrays).TypeError that define
__json__ will now serialize. Classes following the TurboGears
convention (a no-argument __json__) get exactly the behavior
they intended. Classes written for Pyramid’s renderer, whose
__json__(self, request) takes an argument, keep raising
TypeError — now from the missing argument rather than from
default — so code catching the exception keeps working, though
the message changes. A class using the name for something unrelated
would change behavior, but dunder names are reserved by the language
reference, and no plausible unrelated meaning is known._json.make_encoder gains a
dispatch_table argument.__json__ previously
raised TypeError under json.dumps() (unless handled by
default=). Code using default= to serialize them keeps
working, because those objects no longer reach default — but the
built-in representation (array/object) may differ from what a custom
default produced. This is the usual risk of any new default
behavior and is judged acceptable for unambiguous containers.from copyreg import json shadows the json module name in
the importing namespace, as from copyreg import pickle always
has. Documentation will show module-prefixed calls.__raw_json__ and RawJSON emit strings without validation, so a
hook returning attacker-controlled fragments could produce invalid or
misleading JSON. This is no new capability: default= already
executes arbitrary code during encoding, and producing malformed output
requires the application to have installed the hook. The documentation
will note that raw fragments must come from trusted producers.
Four recipes, one per customization level:
__json__:class Money: def __json__(self): return {"amount": str(self.amount), "currency": self.currency}
import copyreg, decimal copyreg.json(decimal.Decimal, str)
copyreg.json(decimal.Decimal, lambda d: copyreg.RawJSON(str(d)))
class APIEncoder(json.JSONEncoder): dispatch_table = {decimal.Decimal: str}
The existing default= remains the call-level catch-all for objects
no other mechanism handled and is documented as running last.
python/cpython#153607 (branch json-customize4), a rebase and rework of the author’s
2017–2022 json-customize branches: the protocol and registry with C
and Python encoder support, standard library __json__ methods, and
tests for both encoder implementations, including the encoding-pipeline
invariant recorded as code comments.
json.register() instead of copyregjson module forces registrants to import
it (~50× the import cost of copyreg, dominated by re),
breaks the pickle symmetry, and gives third-party encoders no
neutral registry. Discoverability is addressed by documentation in
the json docs pointing to copyreg.copyreg.register_json()-style namescopyreg.pickle(), the thirty-year precedent.__json__ for Decimal, namedtuple, array.array, datetime__iter__ (sets, generators, file
objects) would begin serializing as an array instead of reaching
default= or raising. Duck typing therefore applies only to hook
results — an explicit opt-in.isinstance(o, RawJSON) instead of __raw_json____json__ with zero imports would need to import
copyreg the moment it needs raw output. With the duck-typed
protocol, the class is the convenience, not the mechanism.__newobj__ and __newobj_ex__ callables in reduce tuples by
their __name__, but this PEP does not follow that example.
Recognizing raw wrappers by type(o).__name__ == "RawJSON" would
inspect an ordinary, plausible class name on arbitrary objects: any
unrelated class that happens to use it would silently change
encoding behavior. The duck-typed __raw_json__ keeps the
marker in the reserved dunder namespace.__raw_json__ directly on all objects__index__,
__iter__, etc.) and to default() — a real cost in one of
the hottest paths in the standard library. The gated design makes
the raw object pay one method call instead.__repr__/__str__ or a generic __serialize____serialize__
likewise cannot say which format it produces. __str__ can,
however, serve as the payload once a separate marker exists — see
Open Issues.copyreg.dispatch_table. Subclass dispatch is the protocol’s
job: a base class defines __json__ once and subclasses inherit
it. Exact matching keeps lookup one dict access and avoids MRO
scans on the hot path.__json__ applies to dict
keys; requests exist (python/cpython#63020, python/cpython#117391,
python/cpython#85741, python/cpython#117592). A compatible future
extension is to consult the hooks exactly where the keys must be
str... TypeError is raised today (before the skipkeys
skip), so conforming keys pay nothing; the hook result would re-enter
key normalization, and container or raw results would be rejected.
Deferred from this PEP.RecursionError) rather than reporting Circular reference
detected. Tracking hook results in the check_circular markers
would close this at some cost to the hook path; deferred pending
evidence it matters in practice.__raw_json__ as the marker but takes the emitted fragment from
str(o) instead of the method’s return value — RawJSON
defines __str__ returning the fragment for exactly this reason,
and the same would hold if raw wrappers were recognized by identity
or name. Calling the __str__ slot is faster than a full method
call, but the design would require every raw wrapper to define
both __raw_json__ and __str__.This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | PEP 838: Adding python-version to pyvenv.cfg | 0 | 10.69 | 15-07-2026 |
| 2 | PEP 839: PyFrozenSetWriter and PyFrozenDictWriter C API | 0 | 9.93 | 15-07-2026 |
| 3 | PEP 835: Shorthand syntax for Annotated type metadata | 0 | 12.22 | 12-06-2026 |
| 4 | PEP 841: Adding Frozen Syntax to Optimize Immutable Types | 0 | 12.33 | 20-07-2026 |
| 5 | sysutils/py-mqttwarn - 0.36.1 | 0 | 5 | 13-07-2026 |
| 6 | PEP 842: Module Exports | 0 | 11.23 | 25-07-2026 |
| 7 | splitstack/invariants | 0 | 24.29 | 03-08-2026 |
| 8 | ERC-8307: Smart Contract Emergency State | 0 | 7.86 | 28-07-2026 |
| 9 | Elastic Stack 9.3.7 released | 0 | 11.89 | 30-06-2026 |
| 10 | EIP-8337: Validated EVM Code | 0 | 6.96 | 14-07-2026 |