ujson review
ujson is a C extension that exposes JSON `loads`, `dumps`, `load`, and `dump` functions with an interface close to Python's standard `json` module. It adds switches for escaping HTML characters, escaping forward slashes, byte rejection, indentation, and ASCII output. The project itself supplies the decisive warning: its architecture is hard to change without risking security bugs, so development is maintenance-only and the maintainers recommend migration to orjson. Version 5.13.0 adds Python 3.15 and manylinux2014 wheels, restores GraalPy macOS arm64 wheels, disables the global interpreter lock for free-threaded Python, tightens UTF-8 checks when byte encoding is explicitly enabled, and moves package metadata into `pyproject.toml`.
Do not add ujson to a new codebase; its maintainers give a direct security-architecture reason to migrate away. Keep a pinned installation only where measured compatibility work blocks that migration today.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 1 MB |
| Import | ✓ | import ujson in 0.03s · compiled extensions · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does ujson install cleanly?
Yes. In a fresh container with an empty cache, pip install ujson finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does ujson need to run?
Python >=3.10, and a platform wheel with compiled extensions. In our run import ujson succeeded in 0.03s.
ujson or orjson: which should you use?
orjson: Choose the replacement recommended by ujson's maintainers when bytes output fits your boundary. Do not add ujson to a new codebase; its maintainers give a direct security-architecture reason to migrate away.
When should you not use ujson?
This is a new dependency choice. The maintainers call ujson maintenance-only and explicitly recommend orjson because this C architecture can introduce buffer-overflow vulnerabilities.
Use it if
- An existing service already depends on ujson's exact string output and a measured migration regression is harder to absorb immediately.
- Your own production payload benchmark proves it still removes meaningful CPU time while an orjson migration is scheduled.
- A short compatibility window needs a `json`-like encoder that returns `str`, with pins and regression tests around every stored or signed payload.
- This is a new dependency choice. The maintainers call ujson maintenance-only and explicitly recommend orjson because this C architecture can introduce buffer-overflow vulnerabilities.
- You picked it from the word 'fast' rather than a profile. The project's own benchmark table shows orjson ahead on its listed workloads, and standard `json` may already be sufficient.
- Exact standard-library compatibility matters. Defaults such as escaped forward slashes and accepted non-finite numbers can change bytes, signatures, cache keys, and snapshots.
- Static type metadata is required. Our installed 5.13.0 package did not include `py.typed`, so typed projects need their own boundary or stubs.
- Your deployment target lacks a matching wheel or forbids bundled native code. A source install needs a C/C++ toolchain, and system double-conversion builds add ABI compatibility work.
Setup reality
We installed ujson 5.13.0 in a clean Python 3.12 Bookworm container. pip completed in 0.2 seconds and left one package using 1 MB. The package declares no direct dependencies and requires Python 3.10 or newer. It ships a compiled .so, did not include py.typed, and had unknown license metadata in our check. import ujson completed in 0.03 seconds, and pip-audit found no known vulnerabilities. PyPI metadata also leaves the license field empty, while GitHub does not return a recognized SPDX license.
A supported wheel avoids local compilation. Version 5.13.0 adds manylinux2014, Python 3.15, and GraalPy macOS arm64 artifacts, but less common platforms can still build from source. The default build bundles double-conversion and strips Linux debug symbols. Distribution packagers may set UJSON_BUILD_DC_INCLUDES and UJSON_BUILD_DC_LIBS to use a system copy, which makes the resulting extension dependent on that library's ABI. UJSON_BUILD_NO_STRIP=1 keeps debug symbols when crash diagnosis matters.
Treat any swap between json, ujson, and orjson as a serialization change. ujson escapes forward slashes by default, emits ASCII escapes by default, accepts NaN and Infinity unless allow_nan=False, and rejects bytes unless reject_bytes=False. That last escape hatch now has tighter UTF-8 validation in 5.13.0, yet decoded bytes still need explicit compatibility tests. Compare Unicode, surrogate handling, integer limits, float output, non-string keys, custom defaults, sorting, and exact separators on real payloads before changing stored JSON or signed messages.
The README says calls are safe across threads because the module has no global state, and 5.13.0 supports free-threaded execution without the global interpreter lock. Mutating an object in one thread while another serializes it is explicitly unsafe. Take an immutable snapshot or coordinate access. More importantly, no current audit finding cancels the project's architectural warning. Pin the package, keep untrusted-input exposure bounded, and plan migration rather than expanding its footprint.
Patterns
Serialize a Python value encode-json-string
import ujson
payload = ujson.dumps({'id': 42, 'active': True})
assert isinstance(payload, str)Test exact output before replacing `json.dumps` in signatures, snapshots, or persisted data.
Parse a JSON document decode-json-string
record = ujson.loads('{"id":42,"active":true}')
assert record['id'] == 42Apply input-size and nesting limits outside ujson when parsing untrusted data.
Write JSON to a text file write-json-file
with open('result.json', 'w', encoding='utf-8') as stream:
ujson.dump({'status': 'ok'}, stream, ensure_ascii=False)Use text mode with an explicit encoding. Write through a temporary file and rename when partial files are unacceptable.
Load JSON from a file object read-json-file
with open('result.json', encoding='utf-8') as stream:
result = ujson.load(stream)`load` expects a file-like object with `read`; passing a path string is not the same operation.
Keep UTF-8 characters readable emit-unicode
text = ujson.dumps({'city': 'München'}, ensure_ascii=False)
assert text == '{"city":"München"}'The default escapes non-ASCII code points. Ensure the storage or transport is explicitly UTF-8 before disabling that behavior.
Avoid escaped URL slashes keep-url-slashes
text = ujson.dumps(
{'url': 'https://example.com/a'},
escape_forward_slashes=False,
)ujson escapes forward slashes by default. Both forms are JSON, but exact strings differ.
Encode HTML-sensitive characters escape-html-characters
text = ujson.dumps(
'<script>one&two</script>',
encode_html_chars=True,
)This escapes selected characters in the JSON string. It is not an HTML sanitizer or a safe substitute for context-aware output encoding.
Indent diagnostic output pretty-print-json
print(ujson.dumps({'items': [1, 2]}, indent=2, ensure_ascii=False))Pretty output costs more bytes and should not be used in a hot transport path without a reason.
Produce sorted object keys sort-object-keys
text = ujson.dumps({'b': 2, 'a': 1}, sort_keys=True)
assert text == '{"a":1,"b":2}'Sorting alone does not create a cross-library canonical JSON format. Number and escaping behavior still need agreement.
Require standard JSON numbers reject-non-finite-numbers
import math
try:
ujson.dumps({'value': math.nan}, allow_nan=False)
except OverflowError:
handle_invalid_number()Without `allow_nan=False`, ujson can emit NaN and Infinity, which strict JSON consumers may reject.
Convert a datetime explicitly encode-custom-type
from datetime import datetime
def encode(value):
if isinstance(value, datetime):
return value.isoformat()
raise TypeError(type(value).__name__)
text = ujson.dumps({'createdAt': datetime.now()}, default=encode)A default callback should raise for unknown objects so accidental stringification does not hide a schema error.
Gate a move to the standard library compare-migration-output
import json
import ujson
def assert_compatible(value):
old = ujson.loads(ujson.dumps(value, ensure_ascii=False))
new = json.loads(json.dumps(value, ensure_ascii=False))
assert old == newRound-trip equality is a starting point. Also compare exact serialized bytes wherever ordering, escaping, hashing, or signing matters.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| orjson | PyPI | Choose the replacement recommended by ujson's maintainers when bytes output fits your boundary. |
| msgspec | PyPI | Choose it when fast JSON should include typed structs and runtime decoding checks. |
| python-rapidjson | PyPI | Choose it for RapidJSON-specific number, datetime, UUID, and parse-mode controls. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

