I have submitted PEP 837 (PR python/peps#5031 until it renders), which proposes an extension mechanism for the json encoder. It is a revival and rework of an idea with a long history — the implementation descends from my 2017 patch on gh-71549, and the idea itself has been proposed independently since 2010.
In fact, I published a proof-of-concept patch on the tracker in 2017 and have had a full implementation in my local branches since later that year. For this proposal I reworked some design details and wrote the documentation. I tried to write a PEP earlier, but it was too difficult for me — this time I finally managed to finish it.
Summary
Serialization of custom types can be customized at three levels, each covering a different need:
-
Library level — a class defines the JSON representation of its instances with a
__json__()method. No imports, inherited by subclasses:class Money: def __json__(self): return {"amount": str(self.amount), "currency": self.currency} -
Application level — a serialization function for a type you do not control is registered with
copyreg.json(), following thecopyreg.pickle()precedent. Thecopyreg.RawJSONwrapper emits an already encoded fragment verbatim, so an application can finally choose howDecimalserializes:copyreg.json(decimal.Decimal, str) # "1.10" copyreg.json(decimal.Decimal, lambda d: copyreg.RawJSON(str(d))) # 1.10 -
Call level — a particular encoder overrides the global registry with a
dispatch_tableattribute, mirroringpickle.Pickler.dispatch_table.default=is unchanged and runs last.
The more specific level takes precedence. deque, mappingproxy, ChainMap, UserDict, UserList and UserString — containers with an unambiguous JSON form — serialize out of the box. Exact basic types pay no overhead: the hooks are only consulted for objects that would otherwise head toward default= or TypeError.
Reference implementation: python/cpython#153607 (both the C and Python encoders, tests, documentation).
Deliberate non-goals
To keep the discussion focused, a few things are intentionally not in the PEP:
- No default
__json__forDecimal,namedtuple,array.arrayordatetime— each has at least two legitimate JSON representations (number vs string; array vs object; array vs encoded string; many date formats). The registry exists precisely so the application declares this policy; the standard library should not guess. - Dict keys are not covered — the hooks apply to values only. A compatible extension for keys is sketched in Open Issues.
copyreg.copy()/copyreg.deepcopy()for__copy__-like hooks fit the same pattern but will be a separate proposal.
Prior discussions
This idea has come up many times; I have tried to address the points raised in all of them (see the PEP’s Motivation and Rejected Ideas):
- Json object-level serializer (python-ideas, 2010)
- gh-71549 (tracker, 2016) and two dozen related issues listed in the PEP’s appendix
- adding support for a “raw output” in JSON serializer (python-ideas, 2019)
- Improvement: json (python-ideas, 2020)
- Json.register()? (2022)
- Introduce a json magic method (2022–2024)
The ecosystem already uses this convention informally: TurboGears serializes via a no-argument __json__() (with custom_encoders taking precedence over it — the same ordering as this PEP), Pyramid’s renderer calls __json__(request), simplejson added for_json() (choosing that name because dunders are reserved for the language), and projects like conda, Checkmk and TatSu define __json__ today.
Feedback particularly welcome on
- The registry’s home:
copyreg.json()versus a function in thejsonmodule (the PEP argues forcopyregon import cost, pickle symmetry and neutrality for third-party encoders). - The duck-typed
__raw_json__as the raw-output marker (see the alternatives in Rejected Ideas and the__str__-payload variant in Open Issues). - Whether dict-key customization should be deferred (as proposed) or included.