extjson
The extjson library is a small, dependency-free Python module for serializing and deserializing data
to/from JSON, following MongoDB Extended JSON
conventions.
On top of standard JSON types, it supports:
datetime.datetime(timezone-aware only)bytesuuid.UUIDdecimal.Decimal(where available)- special floating-point values (
NaN,Infinity,-Infinity), as standard-compliant$numberDoublewrappers
It only implements a subset of the Extended JSON specification, see Supported Extended JSON wrappers.
Installation
pip install extjson
MicroPython
extjson.py is a single file: copy it to your device's lib/ folder (e.g. mpremote cp extjson.py :lib/),
or install it with mip install github:pakal/extjson/extjson.py.
It also needs some micropython-lib modules, unless your firmware already provides them:
| Module | Status | Install with | Purpose |
|---|---|---|---|
__future__ |
NECESSARY | mip install __future__ |
from __future__ import annotations statement |
datetime |
NECESSARY | mip install datetime |
$date encoding/decoding |
uuid |
NECESSARY | mip install uuid |
$uuid and binary subtype 4 encoding/decoding |
typing |
OPTIONAL | mip install github:josverl/micropython-stubs/mip/typing.mpy |
Not used at runtime |
decimal |
OPTIONAL | any third-party port providing decimal.Decimal |
$numberDecimal decoding (else ImportError) |
On a device, run these as mpremote mip install .... With the Unix port, run them as micropython -m mip install ....
Quick start
from datetime import datetime, timezone
from decimal import Decimal
import uuid
from extjson import convert_from_extjson, convert_to_extjson
payload = {
"id": uuid.UUID("f47ac10b-58cc-4372-a567-0e02b2c3d479"),
"created_at": datetime(2024, 1, 16, 12, 30, tzinfo=timezone.utc),
"price": Decimal("19.99"),
"raw": b"hello",
}
ext_doc = convert_to_extjson(payload) # JSON-compatible tree of dicts/lists/scalars
roundtrip = convert_from_extjson(ext_doc)
assert roundtrip == payload
Canonical vs relaxed mode
All encoding functions take a canonical argument:
- Relaxed mode (
canonical=False, the default): uses native JSON numbers, ISO-formatted dates and simple$uuidwrappers wherever possible. More readable. - Canonical mode (
canonical=True): wraps every number, date and UUID in a strict, type-preserving wrapper ($numberInt,$numberLong,$numberDouble,$datewith$numberLong,$binary...). Less readable, but unambiguous to parse.
Strings, booleans and None are left as-is in both modes. The decoder transparently accepts both formats.
from extjson import convert_to_extjson
print(convert_to_extjson(42)) # 42
print(convert_to_extjson(42, canonical=True)) # {'$numberInt': '42'}
High-level JSON helpers
To handle JSON strings, bytes or files directly:
| Encoding | Decoding | Notes |
|---|---|---|
dumps(obj, **json_kwargs) |
loads(data, **json_kwargs) |
Drop-in replacements for json.dumps/json.loads |
dump_to_json_str(data, **json_kwargs) |
load_from_json_str(data, **json_kwargs) |
Same, with sort_keys=True by default for reproducible output |
dump_to_json_bytes(data, **json_kwargs) |
load_from_json_bytes(data, **json_kwargs) |
UTF-8 encoded bytes |
dump_to_json_file(path, data, **json_kwargs) |
load_from_json_file(path, **json_kwargs) |
UTF-8 file; dump_to_json_file also returns the written bytes |
Encoding functions accept the canonical argument; all functions forward the remaining keyword
arguments to the underlying json.dumps/json.loads (e.g. indent=2 for pretty-printing).
import uuid
from extjson import dump_to_json_str, load_from_json_str
payload = {"name": "hello", "blob": b"xyz", "uid": uuid.uuid4()}
json_text = dump_to_json_str(payload, indent=2)
back = load_from_json_str(json_text)
assert back == payload
Decoding with stdlib json.loads
If you already use Python's stdlib json module, you can decode Extended JSON on the fly
by passing extjson_decoder_object_hook as object_hook:
import json
from extjson import extjson_decoder_object_hook
raw = '{"uid": {"$uuid": "7c0b18f5f4104e839263b38c2328e516"}, "blob": {"$binary": {"base64": "eHl6", "subType": "00"}}}'
decoded = json.loads(raw, object_hook=extjson_decoder_object_hook)
print(decoded)
# {'uid': UUID('7c0b18f5-f410-4e83-9263-b38c2328e516'), 'blob': b'xyz'}
There is no encoding counterpart (i.e. no json.dumps(default=...) hook): Python's JSON encoder
handles some values (e.g. NaN and infinite floats) with its own built-in logic, without ever calling
default, so such a hook could not reliably emit Extended JSON wrappers. Use convert_to_extjson(...)
or dumps(...) instead.
Supported Extended JSON wrappers
Only the following $-wrappers are recognized on decoding; any other single-key dict
(e.g. $oid, $regex, $code, $timestamp, $minKey/$maxKey, $dbPointer, $symbol)
is left untouched, as extjson does not target full MongoDB/BSON compatibility:
| Wrapper | Decodes to | Encoded from |
|---|---|---|
$date (ISO string) |
datetime.datetime, with the offset given in the string |
datetime.datetime (relaxed mode) |
$date ({"$numberLong": ...} milliseconds) |
datetime.datetime in UTC |
datetime.datetime (canonical mode) |
$binary (subType "00") |
bytes |
bytes (both modes) |
$binary (subType "04") |
uuid.UUID |
uuid.UUID (canonical mode) |
$uuid |
uuid.UUID |
uuid.UUID (relaxed mode) |
$numberInt / $numberLong |
int |
int (canonical mode; $numberInt within the 32-bit range, else $numberLong) |
$numberDouble |
float |
float (canonical mode; also relaxed mode for NaN/Infinity/-Infinity) |
$numberDecimal |
decimal.Decimal |
decimal.Decimal (both modes) |
$undefined |
None |
(never; None always encodes to JSON null) |
bytes and decimal.Decimal use the same wrapper in both modes, because Extended JSON has no relaxed
form for them.
A recognized wrapper with a wrongly-typed payload (e.g. {"$numberInt": 42}, or a $binary subtype other
than 00/04, such as the legacy UUID subtype 03) raises TypeError on decoding.
A well-typed but malformed payload lets the error of the underlying parser propagate instead: mostly
ValueError and its subclasses (e.g. {"$numberInt": "1.5"}, {"$uuid": "nope"}, an unparsable $date
string, invalid base64 raising binascii.Error), but also OverflowError for out-of-range $date
milliseconds, and decimal.InvalidOperation for an invalid $numberDecimal.
Notes and behavior details
- Datetimes are truncated to millisecond precision in both modes, so microseconds are lost on round-trip.
- Naive datetimes raise
TypeError, both when encoding adatetimeand when decoding a$datestring without an offset. For naive datetimes, or separate dates/times, store your own formatted strings (e.g.isoformat()) instead. - In relaxed mode, a zero UTC offset is written as
Z; other offsets are preserved. - Tuples are encoded as JSON arrays, and therefore decode as lists.
NaNvalues round-trip, butNaN != NaNstill applies in Python comparisons.- Dict keys are not converted, so only
json-compatible keys work. - Subclasses of supported types (e.g.
enum.IntEnum) are encoded like their base type and decode as that base type. - Values of other types (e.g.
datetime.timedelta) are passed through unchanged, sojson.dumps()raises its usualTypeError: ... not JSON serializable, unless you pass your owndefault=callback todumps(...)or thedump_to_json_*helpers. load_from_json_bytesandload_from_json_fileignore a leading UTF-8 byte-order mark (BOM).extjson.pydegrades gracefully on MicroPython (see MicroPython for required modules):- it imports even without the
typinganddecimalmodules; - if no
decimalport is installed, decoding a$numberDecimalraisesImportError; sort_keysis not applied by thedump_to_json_*helpers;- on single-precision float builds,
$numberDoublevalues can't round-trip exactly.
- it imports even without the
Development
poetry install
poetry run pytest test_extjson.py # current interpreter
poetry run pytest test_extjson.py --cov # with branch coverage report
poetry run tox # all supported Python versions
poetry run ruff check . && poetry run ruff format --check .
License
MIT, see LICENSE.
Metadata
Release files for extjson 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| extjson-0.2.0.tar.gz | 22.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| extjson-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.3 kB
Release files / extjson-0.2.0.tar.gz
| Download URL | extjson-0.2.0.tar.gz |
|---|---|
| Size | 22.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ce333a012c6ab721c500227a67dcde9a73cb65b9755f6a3e7b43a65f0bdd2ef6
|
|
BLAKE2b-256 checksum How to use checksums |
a5774b88947bc1bcbd93d71246b6765e893903cf2ae340456cf92f9b9b5c7f18
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.4
|
Release files / extjson-0.2.0-py3-none-any.whl
| Download URL | extjson-0.2.0-py3-none-any.whl |
|---|---|
| Size | 11.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1ceffd8c50643f2641f378aef7c59beb4dda72fca7629e4a19098365e895de2d
|
|
BLAKE2b-256 checksum How to use checksums |
7964967c8bfad131793964addf153fc7f6b96879fb2612282c183061d9b68dd2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.4
|
Security for extjson 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.