Skip to main content

mojson

A JSON serializer for CPython written in Mojo, with an orjson-style API. NaN / Infinity are written the way Python's json writes them (NaN, Infinity, -Infinity), and NumPy arrays and scalars are supported directly.

Install

The package is published on PyPI as yjson (the name mojson is too close to an existing project); the module you import is still mojson. Wheels are built for CPython 3.11 - 3.15 on x86-64 Linux (manylinux_2_35, i.e. glibc 2.35+ such as Ubuntu 22.04 or newer) and need a CPU with AVX2. They bundle the Mojo runtime library, so nothing else is required:

pip install yjson         # or: uv add yjson
python -c 'import mojson; print(mojson.dumps({"ok": True}))'

Building from the sdist needs the Mojo compiler on PATH (see below).

Build

Requirements: CPython 3.11, 3.12, 3.13, 3.14 or 3.15 (default GIL builds) with headers, a C compiler, x86-64 with AVX2, and Mojo 1.1 (pip install mojo). The build targets one interpreter at a time: it uses .venv-bench/bin/python if present, otherwise python3; override with PYTHON=....

./build.sh                               # -> build/mojson.cpython-312-x86_64-linux-gnu.so
PYTHON=python3.14 ./build.sh             # -> build/mojson.cpython-314-x86_64-linux-gnu.so
export PYTHONPATH="$PWD/build${PYTHONPATH:+:$PYTHONPATH}"

The output carries the interpreter's extension suffix, so builds for several versions coexist in build/ and each interpreter imports its own. The Mojo loops read CPython object fields directly; build.sh probes those offsets from the target headers (src/layout_probe.c) and passes them to both compilers. The C shim re-checks them with _Static_assert, and the module verifies them against live objects at import, so an unsupported interpreter fails with an ImportError instead of reading memory wrongly. The layout differences in this range are small: 3.11 has a longer str header (the legacy wstr field) and keeps an int's sign and digit count in ob_size, for which the build selects a tag-synthesizing variant that leaves the 3.12+ code untouched; 3.14 moved tuple items by adding a cached tuple hash.

Use

import mojson
mojson.dumps({"a": [1, 2.5, None], "b": float("nan")})   # b'{"a":[1,2.5,null],"b":NaN}'
mojson.dumps(obj).decode()                                # if you need a str
mojson.loads(b'{"a": 1}')                                # -> {"a": 1}

See examples/usage.py (NumPy, NaN, files, errors).

dumps(obj, /, default=None, option=None) returns bytes. In addition to the basic JSON types, it supports datetime/date/time, UUID, Enum, dataclass instances, and subclasses of str/int/list/dict. Dataclass fields beginning with _ are omitted. Tuple subclasses use default rather than being treated as arrays. Encoding errors are JSONEncodeError, an alias of TypeError; exceptions raised by a default callback are attached as __cause__.

import datetime
import decimal
import mojson

data = {"created": datetime.datetime(2024, 1, 2), "amount": decimal.Decimal("12.50")}
out = mojson.dumps(
    data,
    default=str,
    option=mojson.OPT_NAIVE_UTC | mojson.OPT_UTC_Z | mojson.OPT_INDENT_2,
)
mojson.dumps({2: "b", 1: "a"}, option=mojson.OPT_NON_STR_KEYS | mojson.OPT_SORT_KEYS)
mojson.dumps({"cached": mojson.Fragment(b'{"a":1}')})

Options use the same bit values as orjson: OPT_INDENT_2, OPT_SORT_KEYS, OPT_NON_STR_KEYS, OPT_STRICT_INTEGER, OPT_APPEND_NEWLINE, OPT_NAIVE_UTC, OPT_UTC_Z, OPT_OMIT_MICROSECONDS, and OPT_PASSTHROUGH_DATACLASS, OPT_PASSTHROUGH_DATETIME, OPT_PASSTHROUGH_SUBCLASS. OPT_SERIALIZE_NUMPY is accepted; NumPy support is already automatic. The deprecated OPT_SERIALIZE_DATACLASS and OPT_SERIALIZE_UUID are zero. Flags can be combined with |.

Integers support the range -2**63 through 2**64 - 1; strict mode restricts values to -(2**53 - 1) through 2**53 - 1. Non-string integer keys retain the 64-bit range in strict mode. Non-string key conversion preserves duplicate JSON keys. Fragments insert their contents verbatim, including under indentation; validate the contents yourself when needed.

OPT_INDENT_2, OPT_SORT_KEYS and OPT_NON_STR_KEYS run on the same compiled writers as the compact default (indentation is a compile-time variant of those loops; sorting and non-str keys snapshot the dict as native records), so they stay close to orjson's speed for the same option. OPT_STRICT_INTEGER and OPT_PASSTHROUGH_SUBCLASS use a generic traversal that checks every value and is slower. Custom conversions and uncommon types cost additional work; measure their speed on your payload (bench/bench_shapes.py). loads uses Python's standard parser with UTF-8, nonfinite-number, and surrogate checks. It accepts str/bytes/bytearray/contiguous memoryview and raises JSONDecodeError (a subclass of json.JSONDecodeError). Its parsing speed and maximum nesting follow the stdlib backend, rather than orjson's parser.

dumps_socket(obj, /, default=None) is a separate native encoder for Reflex wire packets. It accepts arbitrary-size integers (subject to CPython's decimal digit limit), writes NaN/Infinity as bare tokens and escapes lone surrogates, matching the stdlib json.dumps wire with compact separators. It returns bytes and accepts no formatting options. Framework custom types use default. Reflex installs it as reflex[yjson]: its format.json_dumps calls this function for compact output, with no stdlib retries or Python container walks. Ordinary dumps() keeps its integer range and UTF-8 error behavior.

Test and benchmark

pip install orjson numpy
python tests/check_correctness.py path/to/jsonexamples     # corpus optional
python tests/check_features.py                            # options, types, callbacks, decoding
python bench/bench_paired.py path/to/jsonexamples          # mojson vs orjson, robust
python bench/bench_features.py                            # enabled feature paths vs orjson
python bench/bench_shapes.py --cpu 2                      # per payload shape vs orjson (where it wins or loses)
python bench/bench_regression.py                          # requires a baseline build in build/baseline/
python bench/bench_all_libraries.py path/to/jsonexamples   # + msgspec, ujson, rapidjson, json, simplejson
python bench/bench_numpy.py

Benchmark corpus: jsonexamples/ from https://github.com/simdjson/simdjson-data.

The Reflex PR 6116 comparison (as of v0.1.1; its integration scripts used the marker wire that dumps_socket no longer writes, and were removed) documents a pinned framework checkout, unchanged upstream codec tests, paired encode/event benchmarks, and real browser checks for both dump backends. Published measurements and validation are in bench/results, including the general 14-file comparison, native socket benchmarks and the default-path regression check. The same directory holds the CPython 3.11 - 3.15 measurements and the profile-driven improvements measured on 3.14 (corpus 1.18× faster than before them; 1.28–1.32× of orjson on every interpreter). The all-events Reflex run measures every workload of the PR's event benchmark: all at parity, because those deltas spend 88–97% of their encode time in Reflex's Python default() serializer for pydantic models, which neither codec can skip. The orjson baseline retains the PR's original codec; mojson replaces its socket retry logic with native serialization. Earlier option and indentation results remain in build/fix-options.json and build/indent-reflex-benchmark.json.

Packaging and CI

pyproject.toml describes the package; setup.py only teaches setuptools to compile the extension through build.sh, so uv build and pip install . work with the Mojo compiler on PATH. The dependency groups test, wheel and mojo are pinned in uv.lock:

UV_PROJECT_ENVIRONMENT=.venv-mojo uv sync --only-group mojo --python 3.13   # Mojo 1.1
export PATH="$PWD/.venv-mojo/bin:$PATH"
uv build --sdist
uv build --wheel --python 3.13 --out-dir dist dist/yjson-*.tar.gz            # cp313 wheel
uv sync --only-group wheel                                                   # auditwheel + patchelf into .venv
PATH="$PWD/.venv/bin:$PATH" auditwheel repair \
    --ldpaths "$(.venv-mojo/bin/python -c 'import modular; print(modular.__path__[0] + "/lib")')" \
    --disable-isa-ext-check --wheel-dir wheelhouse dist/*.whl

auditwheel repair copies libKGENCompilerRTShared.so and its dependencies into mojson.libs/ and sets the manylinux_2_35 tag (the floor set by the Mojo runtime). --disable-isa-ext-check is required because the extension targets x86-64-v3 (AVX2) by design.

.github/workflows/ci.yml runs the same steps on every push and pull request: it builds the sdist, builds one wheel per interpreter from that sdist, repairs it, installs it into a clean environment and runs tests/check_features.py, tests/check_correctness.py (with the simdjson corpus) and examples/usage.py against the installed wheel. Pushing a tag vX.Y.Z whose version matches pyproject.toml additionally publishes the sdist and wheels to PyPI through trusted publishing from the pypi GitHub environment, so no API token is stored. To release: bump version in pyproject.toml, commit, then git tag v0.1.0 && git push origin v0.1.0.

Local comparison with uv

If Mojo is already installed in .venv, keep that compiler environment and use a separate CPython environment for the extension (3.12 shown; any of 3.11 - 3.15 works):

uv venv --python 3.12 .venv-bench
uv pip install --python .venv-bench/bin/python orjson numpy
PATH="$PWD/.venv/bin:$PATH" ./build.sh
.venv-bench/bin/python tools/fetch_corpus.py
.venv-bench/bin/python tests/check_correctness.py build/jsonexamples
.venv-bench/bin/python tests/check_features.py
.venv-bench/bin/python bench/bench_paired.py build/jsonexamples --cpu 2 --micro --output build/paired-local.json

To build and test every supported version side by side:

for v in 3.11 3.13 3.14 3.15; do
  uv venv --python $v .venv-py$v && uv pip install --python .venv-py$v/bin/python orjson numpy
  PATH="$PWD/.venv/bin:$PATH" PYTHON=.venv-py$v/bin/python ./build.sh
  .venv-py$v/bin/python tests/check_correctness.py build/jsonexamples
  .venv-py$v/bin/python tests/check_features.py
done

Choose an available logical CPU for --cpu, or omit it. The paired benchmark checks output equality, warms both serializers, calibrates a common batch size to at least 10 ms, and alternates order across 40 pairs. It reports median time per call, the median orjson time / mojson time ratio, and ratio quartiles. Ratios above 1 mean mojson is faster. The optional JSON report includes every sample and environment metadata. The corpus downloader records the source revision and SHA-256 hashes in build/jsonexamples/manifest.json.txt.

What's inside (src/mojson.mojo)

  • Direct reads of CPython object layouts (type pointer, list/tuple items, compact ints, float bits, str data) for 3.11 - 3.15, with the offsets supplied by the build and verified at import, and external_call into the CPython C API; output written straight into a bytes object.
  • Floats: Żmij shortest round-trip core (one 64x128 multiply), SSE BCD digit conversion and pshufb decimal-point insertion (ported from zmij), exponent table, 4-float batches.
  • Ints: itoap-style writer, and 4-at-a-time SIMD batches using zmij's 16-bit-lane digit trick.
  • Strings: compact-ASCII / cached-UTF-8 access (as orjson), 64-byte SIMD escape scan with ctz jump, page-safe 32-byte masked tail.
  • Dicts: entries are read straight from CPython's key table (no PyDict_Next call per key; split tables fall back to it), register-resident write cursor, per-call key cache (repeated key objects copy their escaped bytes), and inline true/false/{}/[] values. CPython's key-table kind keeps Unicode-only dictionaries on this writer even with OPT_NON_STR_KEYS. OPT_SORT_KEYS and non-str keys snapshot the entries as native records (stack storage up to 31 entries), sorted with an inlined insertion/quicksort rather than qsort callbacks; int, float, bool and None keys are converted to text without creating Python strings.
  • Lists: 4-wide SIMD batches for ints and floats, and short leaf lists of numbers (coordinate pairs) written in place without a nested call.
  • NumPy: buffer-protocol fast path for contiguous float64/float32/int64/int32/uint8/bool, .tolist() fallback.

tools/ holds the Python reference implementations used to validate the float core (zmij_reference.py, ref.py) and zmij's power-of-ten table.

Limitations

  • NaN/Infinity output intentionally differs from orjson's null. The strict decoder rejects these tokens, so nonfinite output does not round-trip through loads.
  • NumPy float32 values are formatted after promotion to float64, which can produce more decimal digits than orjson. Non-contiguous arrays use .tolist().
  • CPython 3.11 - 3.15 default (GIL) builds only: free-threaded (t) builds lay objects out differently and are rejected at build time. One build serves one minor version. x86-64 AVX2 by default; MCPU=x86-64-v4 ./build.sh builds an AVX-512 variant (k-mask string scanning) that only runs on such CPUs and was slower on a Cascade Lake Xeon (512-bit frequency penalty), so there is no runtime dispatch.
  • Keep build/_mojson_support.py alongside the built mojson.*.so; the build copies this stdlib-only helper automatically. The extension has no orjson runtime dependency.
  • The string tail reads up to 31 bytes past a string's end within the same memory page (safe, but AddressSanitizer/valgrind will flag it).
  • A .so straight from build.sh needs the Mojo runtime libraries (libKGENCompilerRTShared.so, libAsyncRTRuntimeGlobals.so, libMSupportGlobals.so) from the mojo pip package, found through the RUNPATH the compiler records. The published wheels bundle them (auditwheel repair). At import the module points the Mojo runtime at the running interpreter (it sets MOJO_PYTHON_LIBRARY when unset), so no python3 needs to be on PATH. If MOJO_PYTHON or MOJO_PYTHON_LIBRARY is already set, the runtime uses it, so it must be valid.

Credits

Float algorithm and power-of-ten table from Żmij by Victor Zverovich (https://github.com/vitaut/zmij, MIT); integer writer structure from itoap; design informed by orjson's source (https://github.com/ijl/orjson). See THIRD_PARTY_NOTICES.md for the upstream notices.

Metadata

Release files for yjson 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for yjson 0.1.2
File Size Uploaded
yjson-0.1.2.tar.gz 80.8 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for yjson 0.1.2
File
yjson-0.1.2-cp315-cp315-manylinux_2_35_x86_64.whl CPython 3.15 CPython 3.15 Linux glibc 2.35+ x86-64 Details
yjson-0.1.2-cp314-cp314-manylinux_2_35_x86_64.whl CPython 3.14 CPython 3.14 Linux glibc 2.35+ x86-64 Details
yjson-0.1.2-cp313-cp313-manylinux_2_35_x86_64.whl CPython 3.13 CPython 3.13 Linux glibc 2.35+ x86-64 Details
yjson-0.1.2-cp312-cp312-manylinux_2_35_x86_64.whl CPython 3.12 CPython 3.12 Linux glibc 2.35+ x86-64 Details
yjson-0.1.2-cp311-cp311-manylinux_2_35_x86_64.whl CPython 3.11 CPython 3.11 Linux glibc 2.35+ x86-64 Details

Total release size: 6.5 MB

Release files / yjson-0.1.2.tar.gz

Download URL yjson-0.1.2.tar.gz
Size 80.8 kB
Tags Source
SHA-256 checksum
How to use checksums
04f67aa460c2edeb4d0b6c50aaa3b9a0701ad6f8c9faa9174a6efa18998e00d0
BLAKE2b-256 checksum
How to use checksums
e7e75f5b9c065066a228bf1aad5a25a53dd9e790f6d8d4456730bff1a2bf2d0c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / yjson-0.1.2-cp315-cp315-manylinux_2_35_x86_64.whl

Download URL yjson-0.1.2-cp315-cp315-manylinux_2_35_x86_64.whl
Size 1.3 MB
Tags CPython 3.15 Linux glibc 2.35+ x86-64
SHA-256 checksum
How to use checksums
d4020fd19741346e39a45c1c7931ed0f66ac0b3e703f6890e0b33246619bd150
BLAKE2b-256 checksum
How to use checksums
7e4b6b10f463aa7e4ef36f0eed92c821111bff044590a3497e31f285e735ae43
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / yjson-0.1.2-cp314-cp314-manylinux_2_35_x86_64.whl

Download URL yjson-0.1.2-cp314-cp314-manylinux_2_35_x86_64.whl
Size 1.3 MB
Tags CPython 3.14 Linux glibc 2.35+ x86-64
SHA-256 checksum
How to use checksums
71c20e69093ffa2637a49c26025cfccb14fe4cd8b8263ea9e1b689a16a7a4794
BLAKE2b-256 checksum
How to use checksums
bd790cbabada81bf8b3fd44f3650b18f4a5c283dbec1a44bab2b1e67ef287918
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / yjson-0.1.2-cp313-cp313-manylinux_2_35_x86_64.whl

Download URL yjson-0.1.2-cp313-cp313-manylinux_2_35_x86_64.whl
Size 1.3 MB
Tags CPython 3.13 Linux glibc 2.35+ x86-64
SHA-256 checksum
How to use checksums
e65708f806ee0233c3458405ef7b12c1a04601f0a9ba51c36a61e399b2ec8565
BLAKE2b-256 checksum
How to use checksums
584fa6e14e7bb62eb1c326d5e541dc8e825ae21d127eada4481ea230290f4ff6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / yjson-0.1.2-cp312-cp312-manylinux_2_35_x86_64.whl

Download URL yjson-0.1.2-cp312-cp312-manylinux_2_35_x86_64.whl
Size 1.3 MB
Tags CPython 3.12 Linux glibc 2.35+ x86-64
SHA-256 checksum
How to use checksums
68654b608a0b715c732e188e5d5d9e6d00d97f30b914c37572ccc9d13fff723c
BLAKE2b-256 checksum
How to use checksums
aeb66935436878d006bffa184cf5de3afe9d307078192faffeb02d992cfcaf0f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / yjson-0.1.2-cp311-cp311-manylinux_2_35_x86_64.whl

Download URL yjson-0.1.2-cp311-cp311-manylinux_2_35_x86_64.whl
Size 1.3 MB
Tags CPython 3.11 Linux glibc 2.35+ x86-64
SHA-256 checksum
How to use checksums
2510ac66187fab76d068685563f8d3c81f50c6be1770ff5893605ea5f53e57a2
BLAKE2b-256 checksum
How to use checksums
5166c0d1d8ee4037c47203f600ad316a395873a3833cdbc623ad590d9f1e8412
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.3.1

6 release files

0.3.0

6 release files

0.2.0

6 release files

This release

0.1.2 This release

6 release files

0.1.1

6 release files

0.1.0

6 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page