Exemplo
orjson 3.11.9: Swap json for orjson on Alpine, without the wheel surprise or the silent output changes
Amostra verificada para pypi orjson 3.11.9: Swap json for orjson on Alpine, without the wheel surprise or the silent output changes. O contrato rodou em…
sha256:84dc3de1142d9942eb7c819b260089b9174ebd77f0213fd16a72c9f0640eb35e
Esta rede oferece uma coisa: uma amostra que compila. Ela a executou em um sandbox e guardou o recibo assinado. Não classifica nem garante nada — se o mesmo código compila onde você está, ela não mediu.
Quantas chaves de assinatura distintas enviaram um recibo de contrato aprovado. Uma é só o autor; mais de uma significa que outra pessoa também o compilou. Uma chave é gerada por conta própria e não tem identidade registrada por trás, então conta chaves, não pessoas.
MIT-0
Evidência de execução
O ambiente declarado e as execuções assinadas ficam separados, para você ver exatamente o que esta amostra executou e onde.
- Base da evidência
- Contrato assinado aprovado
- Recibos de verificação
- 2
- Chaves de assinatura que o compilaram
- 2
Ambiente declarado
python 3.12 linux · musl x64 python 3.12 python pip
Ambientes das execuções de verificação
| Ambiente | Contrato | Etapas | Execução |
|---|---|---|---|
| python 3.12 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · pypi@1 |
2026-08-14 |
| python 3.12 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · pypi@1 |
2026-08-18 |
Caso
MIGRATION- Objetivo
- Swap json for orjson on Alpine, without the wheel surprise or the silent output changes
- Pacotes
- Símbolos
-
- orjson.dumps
- orjson.loads
- orjson.OPT_SORT_KEYS
- orjson.OPT_NON_STR_KEYS
- orjson.OPT_INDENT_2
- orjson.OPT_UTC_Z
- orjson.JSONEncodeError
- orjson.JSONDecodeError
- json.dumps
- Ambiente
- python 3.12
- Criado
- 2026-08-14T08:53:26Z
Contrato
- assert pip took the musllinux wheel, orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl, reconstructed from the recorded dist-info tag
- assert the distribution is a compiled extension, not pure Python, whose filename is this interpreter's own EXT_SUFFIX, and declares no runtime dependencies of its own
- assert the extension needs exactly one library, libc.so, and no file of that name exists under any directory the loader searches
- assert musl's loader resolves that name to itself, which is the only reason the extension imports
- assert no CPython library is linked, so the Py symbols are resolved from the interpreter that imports it
- assert the same ELF reader finds the versioned musl soname and libpython in the interpreter binary, so the reading is real
- assert the interpreter itself reports musl, with musl's loader present and glibc's absent
- assert orjson.dumps returns bytes where json.dumps returns str, with no separator spaces and no \uXXXX escaping
- assert OPT_INDENT_2 puts the space after the colon back and matches json.dumps(indent=2) byte for byte
- assert there is no orjson.dump or orjson.load, so writing to a file is the caller's job
- assert datetime, date, time and UUID serialize natively as RFC 3339 with +00:00 unless OPT_UTC_Z, where json raises TypeError
- assert orjson accepts default as its second positional argument and json.dumps refuses a positional there, the reverse of the usual warning
- assert every other json.dumps keyword is a TypeError, and sort_keys is instead option=OPT_SORT_KEYS with the same code point ordering
- assert orjson.JSONEncodeError is builtins.TypeError itself while JSONDecodeError is a real subclass of json.JSONDecodeError
- assert non-str dict keys raise unless OPT_NON_STR_KEYS, while json coerces them silently and loses a value when two keys collide
- assert nan and infinity serialize as null with the default hook never consulted, while json emits tokens orjson then refuses to read back
- assert a walk over the payload finds the non-finite floats orjson will not report
- assert a non-finite Decimal raises instead of vanishing, until a default hook floats it and it becomes null too
- assert integers outside -2**63 through 2**64-1 raise where json prints them
Arquivos
- csx.json
- requirements.txt
- src/__init__.py
- src/serialize.py
- src/wheel.py
- test/contract.py
Código-fonte
{"case":{"caseId":"case:sha256:0e6f80dd29b7334021edb467495c7b38703365a96e5aa935c6eb452fc881834f","constraints":{"libc":"musl","runtime":"python"},"contract":["assert pip took the musllinux wheel, orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl, reconstructed from the recorded dist-info tag","assert the distribution is a compiled extension, not pure Python, whose filename is this interpreter's own EXT_SUFFIX, and declares no runtime dependencies of its own","assert the extension needs exactly one library, libc.so, and no file of that name exists under any directory the loader searches","assert musl's loader resolves that name to itself, which is the only reason the extension imports","assert no CPython library is linked, so the Py symbols are resolved from the interpreter that imports it","assert the same ELF reader finds the versioned musl soname and libpython in the interpreter binary, so the reading is real","assert the interpreter itself reports musl, with musl's loader present and glibc's absent","assert orjson.dumps returns bytes where json.dumps returns str, with no separator spaces and no \\uXXXX escaping","assert OPT_INDENT_2 puts the space after the colon back and matches json.dumps(indent=2) byte for byte","assert there is no orjson.dump or orjson.load, so writing to a file is the caller's job","assert datetime, date, time and UUID serialize natively as RFC 3339 with +00:00 unless OPT_UTC_Z, where json raises TypeError","assert orjson accepts default as its second positional argument and json.dumps refuses a positional there, the reverse of the usual warning","assert every other json.dumps keyword is a TypeError, and sort_keys is instead option=OPT_SORT_KEYS with the same code point ordering","assert orjson.JSONEncodeError is builtins.TypeError itself while JSONDecodeError is a real subclass of json.JSONDecodeError","assert non-str dict keys raise unless OPT_NON_STR_KEYS, while json coerces them silently and loses a value when two keys collide","assert nan and infinity serialize as null with the default hook never consulted, while json emits tokens orjson then refuses to read back","assert a walk over the payload finds the non-finite floats orjson will not report","assert a non-finite Decimal raises instead of vanishing, until a default hook floats it and it becomes null too","assert integers outside -2**63 through 2**64-1 raise where json prints them"],"goal":"Swap json for orjson on Alpine, without the wheel surprise or the silent output changes","kind":"MIGRATION","packages":["pkg:pypi/orjson@3.11.9"],"schemaVersion":1,"symbols":["orjson.dumps","orjson.loads","orjson.OPT_SORT_KEYS","orjson.OPT_NON_STR_KEYS","orjson.OPT_INDENT_2","orjson.OPT_UTC_Z","orjson.JSONEncodeError","orjson.JSONDecodeError","json.dumps"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","ecosystem":"pypi","executionContext":"python","language":"python","libc":"musl","os":"linux","packageManager":"pip","runtime":"python","runtimeVersion":"3.12","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/orjson@3.11.9"],"schemaVersion":1,"symbols":["orjson.dumps","orjson.loads","orjson.OPT_SORT_KEYS","orjson.OPT_NON_STR_KEYS","orjson.OPT_INDENT_2","orjson.OPT_UTC_Z","orjson.JSONEncodeError","orjson.JSONDecodeError","json.dumps"],"verifierAdapter":"pypi@1"}
orjson==3.11.9
"""Replacing json.dumps with orjson.dumps, and the differences that change
behaviour without changing your code.
orjson is not a drop-in for json. The signature is dumps(obj, default=None,
option=None) and that is all it takes: every json.dumps keyword other than
default — sort_keys, indent, ensure_ascii, separators, allow_nan, cls — is
either a bit in `option` or does not exist. Passing one is a TypeError, so
those breakages are loud. The ones worth writing a sample about are the
quiet ones:
- dumps returns bytes, not str. Anything doing `+ "\\n"` or handing the
result to a str-typed API breaks; anything writing to a socket or a
binary file takes them unchanged and never notices.
- non-ASCII stays UTF-8. json escapes to \\uXXXX by default, orjson has no
ensure_ascii, so byte-for-byte comparisons against json output fail on
any payload carrying a character above ASCII.
- the spacing is tighter: nothing after ":" or ",". That one is only a
default — under OPT_INDENT_2 the output is json's own indent=2 layout,
byte for byte, which is the cheapest way to keep such a comparison.
- datetime, date, time and UUID serialize natively where json raises
TypeError. The format is RFC 3339 with +00:00, not Z — if a consumer
matched on Z, add OPT_UTC_Z.
- non-str dict keys are refused instead of coerced. json turns {1: "a"}
into {"1": "a"} silently, which is where {1: "a", "1": "b"} becomes two
identical keys and one value disappears on the way back.
- nan and infinity become null, silently. See below.
Two things the folklore gets backwards, corrected by measurement:
`default` positionally. orjson.dumps takes default as its second
positional argument and option as its third, so orjson.dumps(obj, hook)
works. json.dumps is the one that refuses it — every parameter after obj
is keyword-only there, so json.dumps(obj, hook) raises "dumps() takes 1
positional argument but 2 were given". The migration hazard runs the other
way from how it is usually told.
NaN and Infinity. orjson does not reject them. It serializes nan, inf and
-inf as null, with no error and no way to intercept: `default` is only
consulted for types orjson cannot serialize, and float is a type it can,
so the hook is never called. json instead emits bare NaN and Infinity,
which is not JSON — and orjson.loads then refuses to read it back with
orjson.JSONDecodeError. So the two libraries fail in opposite directions
on the same value: json produces something no strict parser accepts,
orjson produces valid JSON that has quietly lost the number. If a NaN in
the payload must not pass silently, the check has to be yours, before the
dumps call.
One more identity worth knowing before writing except clauses: encode errors
are raised as builtins.TypeError itself. orjson.JSONEncodeError is not a
subclass of TypeError, it is the same object, so the two except clauses
catch exactly the same thing. Decode errors are a real subclass, of both
ValueError and json.JSONDecodeError, so existing json error handling keeps
working on the read side.
"""
import orjson
def dumps_text(
obj,
*,
default=None,
sort_keys: bool = False,
non_str_keys: bool = False,
indent: bool = False,
) -> str:
"""A json.dumps-shaped wrapper: str out, keywords mapped onto `option`.
Write this adapter once rather than fixing call sites one at a time. The
options are a bitmask, so they OR together; 0 means "no options", which
orjson accepts in the same position as a real flag.
"""
option = 0
if sort_keys:
option |= orjson.OPT_SORT_KEYS
if non_str_keys:
option |= orjson.OPT_NON_STR_KEYS
if indent:
option |= orjson.OPT_INDENT_2
return orjson.dumps(obj, default, option).decode()
def has_non_finite_float(obj) -> bool:
"""The guard orjson cannot give you: find nan/inf before they become null.
Only floats are walked. int cannot be non-finite; Decimal can —
Decimal("NaN") and Decimal("Infinity") are ordinary values — but orjson
refuses Decimal outright with TypeError, so that one fails loudly and
needs no guard. It goes quiet again if a `default` hook converts the
Decimal to float, which is the case this function does not cover.
"""
if isinstance(obj, float):
return obj != obj or obj in (float("inf"), float("-inf"))
if isinstance(obj, dict):
return any(has_non_finite_float(value) for value in obj.values())
if isinstance(obj, (list, tuple)):
return any(has_non_finite_float(item) for item in obj)
return False
"""What pip actually installed for orjson on Alpine, read off the installed
distribution instead of assumed from the version number.
orjson is a compiled Rust extension, so on a musl image there is exactly one
wheel pip can take: the musllinux one. The wheel that landed here during
resolve was
orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl (136 kB)
and the only receipt for it that survives into an offline stage is the Tag
line in orjson-3.11.9.dist-info/WHEEL. That is what these helpers read.
The trap is what happens when the tag does not match. pip does not say
"wrong platform" — it falls back to the 5.6 MB sdist and tries to build it,
and the failure that follows is not the missing compiler people assume.
Forced here with --no-binary :all:, orjson's build backend (maturin, via
puccinialin) downloads rustup-init for x86_64-unknown-linux-musl, installs
21 MB of toolchain into the root user's .cache/puccinialin, and only then dies on
`cargo --version` returning exit status 127, which pip reports as
metadata-generation-failed. Installing build-base does not touch that. The
real question is which wheel tags exist for the interpreter in front of you.
The surprise from measuring the binary, which is the point of this half of
the sample: the extension is not statically linked, and the one library it
does need does not exist as a file. Its whole DT_NEEDED list is
['libc.so']
and no file called libc.so exists under any directory the loader searches —
the only libc file present is /lib/libc.musl-x86_64.so.1. It loads anyway
because musl's dynamic loader answers to that name as itself; running the
loader by hand on the extension prints "libc.so => /lib/ld-musl-x86_64.so.1".
Carried to a glibc image the same file is stopped twice, and the first stop
is not the interesting one. On python:3.12-slim `import orjson` fails before
any loader runs, with "No module named 'orjson.orjson'", because that
interpreter's EXT_SUFFIX is .cpython-312-x86_64-linux-gnu.so and the file on
disk ends in -musl.so. Push past that with ctypes.CDLL and the DT_NEEDED
name is what stops it: OSError, "libc.so: cannot open shared object file".
That image has no libc.so either, only /usr/lib/x86_64-linux-gnu/libc.so.6.
Both of those were measured on python:3.12-slim; a contract running on
Alpine cannot check them, so what it asserts is this half — the filename
against this interpreter's own EXT_SUFFIX, and the resolution against this
loader.
The extension links nothing for CPython either. Its Py* symbols are left
undefined and resolved from the interpreter that imports it, which is why
python's own binary lists libpython3.12.so.1.0 and the extension lists
nothing of the kind — and why running the loader on it standalone reports
those symbols as missing.
"""
import email
import importlib.metadata as metadata
import os
import subprocess
import sysconfig
def installed_wheel() -> dict:
"""The dist-info receipt: version, wheel tags, purelib flag, deps."""
dist = metadata.distribution("orjson")
wheel = email.message_from_string(dist.read_text("WHEEL") or "")
return {
"version": dist.version,
"tags": wheel.get_all("Tag") or [],
"root_is_purelib": (wheel["Root-Is-Purelib"] or "").strip() == "true",
"requires_dist": dist.metadata.get_all("Requires-Dist") or [],
}
def wheel_filename() -> str:
"""Reconstruct the filename pip downloaded from the recorded tag.
A wheel name is name-version-tag.whl, so a single-tag dist-info pins the
exact file. Comparing against a literal is how the sample proves the
musllinux wheel was used rather than a locally built sdist.
"""
info = installed_wheel()
if len(info["tags"]) != 1:
raise AssertionError(f"expected one wheel tag, got {info['tags']}")
return f"orjson-{info['version']}-{info['tags'][0]}.whl"
def extension_module_path() -> str:
"""Absolute path of the compiled .so, taken from the installed RECORD."""
dist = metadata.distribution("orjson")
shared = [f for f in (dist.files or []) if str(f).endswith(".so")]
if len(shared) != 1:
raise AssertionError(f"expected one .so, got {shared}")
return os.fspath(dist.locate_file(shared[0]))
def elf_needed_libraries(path: str) -> list[str]:
"""DT_NEEDED entries of an ELF64 file, read from its dynamic section.
Walks the program headers to PT_DYNAMIC, collects the DT_NEEDED string
offsets and DT_STRTAB, then maps that virtual address back through the
PT_LOAD segments to a file offset. An empty list means nothing is loaded
at runtime — the only way to check "statically linked" rather than trust
the claim.
"""
with open(path, "rb") as handle:
raw = handle.read()
if raw[:4] != b"\x7fELF":
raise ValueError(f"{path} is not an ELF file")
if raw[4] != 2:
raise ValueError(f"{path} is not 64-bit ELF")
def u16(at: int) -> int:
return int.from_bytes(raw[at:at + 2], "little")
def u64(at: int) -> int:
return int.from_bytes(raw[at:at + 8], "little")
phoff, phentsize, phnum = u64(0x20), u16(0x36), u16(0x38)
loads: list[tuple[int, int, int]] = []
dynamic: tuple[int, int] | None = None
for index in range(phnum):
at = phoff + index * phentsize
p_type = int.from_bytes(raw[at:at + 4], "little")
p_offset, p_vaddr, p_filesz = u64(at + 8), u64(at + 16), u64(at + 32)
if p_type == 1: # PT_LOAD
loads.append((p_vaddr, p_offset, p_filesz))
elif p_type == 2: # PT_DYNAMIC
dynamic = (p_offset, p_filesz)
if dynamic is None:
return []
needed: list[int] = []
strtab: int | None = None
start, size = dynamic
for at in range(start, start + size, 16):
tag = int.from_bytes(raw[at:at + 8], "little", signed=True)
value = u64(at + 8)
if tag == 0: # DT_NULL ends the table
break
if tag == 1: # DT_NEEDED
needed.append(value)
elif tag == 5: # DT_STRTAB
strtab = value
if not needed:
return []
if strtab is None:
raise ValueError("DT_NEEDED without DT_STRTAB")
base = None
for vaddr, offset, filesz in loads:
if vaddr <= strtab < vaddr + filesz:
base = offset + (strtab - vaddr)
break
if base is None:
raise ValueError("DT_STRTAB falls outside every PT_LOAD segment")
names = []
for offset in needed:
at = base + offset
names.append(raw[at:raw.index(b"\x00", at)].decode())
return names
MUSL_LOADER = "/lib/ld-musl-x86_64.so.1"
LOADER_SEARCH_DIRS = ("/lib", "/usr/lib", "/usr/local/lib", "/lib64")
def libc_so_exists() -> bool:
"""Is there a file named libc.so anywhere the loader would look?
Walks the search directories rather than stat-ing four paths, because the
claim being checked is an absence and a shallow check would not earn it.
"""
for root in LOADER_SEARCH_DIRS:
if not os.path.isdir(root):
continue
for _dirpath, _dirnames, filenames in os.walk(root, onerror=lambda error: None):
if "libc.so" in filenames:
return True
return False
def musl_loader_report(path: str) -> str:
"""Ask musl's loader to resolve an ELF file's needs, and read the answer.
`ldso --list` prints the resolved map. It exits non-zero here because the
extension's CPython symbols only exist inside a running interpreter, so
both streams are returned together and the caller reads what it needs.
"""
done = subprocess.run(
[MUSL_LOADER, "--list", path],
capture_output=True,
text=True,
check=False,
)
return done.stdout + done.stderr
def interpreter_libc() -> dict:
"""Evidence that this interpreter is a musl one, not just named alpine."""
return {
"soabi": sysconfig.get_config_var("SOABI"),
"host": sysconfig.get_config_var("HOST_GNU_TYPE"),
"ext_suffix": sysconfig.get_config_var("EXT_SUFFIX"),
"musl_loader": os.path.exists(MUSL_LOADER),
"glibc_loader": os.path.exists("/lib64/ld-linux-x86-64.so.2"),
"musl_libc_file": os.path.exists("/lib/libc.musl-x86_64.so.1"),
"libc_so_file": libc_so_exists(),
}
import datetime as dt
import json
import os
import sys
import uuid
from decimal import Decimal
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import orjson
from src.serialize import dumps_text, has_non_finite_float
from src.wheel import (
elf_needed_libraries,
extension_module_path,
installed_wheel,
interpreter_libc,
musl_loader_report,
wheel_filename,
)
def raises(expected, call):
"""Run call, require `expected`, and hand the exception back for checking."""
try:
call()
except expected as caught:
return caught
raise AssertionError(f"expected {expected.__name__}, nothing was raised")
# --- packaging: which wheel is installed on this image --------------------
info = installed_wheel()
# pip took the musllinux wheel: one tag, matching this interpreter exactly.
# A manylinux tag cannot match on Alpine, and no match at all means pip
# falls back to the sdist, which on python:3.12-alpine downloads a Rust
# toolchain of its own and then fails anyway — src/wheel.py has the measured
# log. The filename here is reconstructed from the recorded tag rather than
# scraped from a log, so it can be checked offline.
assert info["tags"] == ["cp312-cp312-musllinux_1_2_x86_64"], info["tags"]
assert wheel_filename() == "orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl"
# It is a compiled extension, so the wheel choice is load bearing.
assert info["root_is_purelib"] is False
extension = extension_module_path()
libc = interpreter_libc()
assert os.path.basename(extension) == "orjson.cpython-312-x86_64-linux-musl.so"
# That filename is this interpreter's own EXT_SUFFIX, which is what makes
# import find it: a glibc CPython looks for a name ending in -gnu.so and
# never sees this file at all. The wheel is refused by name before any
# loader is asked anything.
assert os.path.basename(extension) == "orjson" + libc["ext_suffix"]
# And it pulls in nothing else, which is why a --no-deps requirements.txt of
# one line is the complete transitive closure.
assert info["requires_dist"] == []
# Measured, against the expectation that a musllinux wheel would be
# statically linked: it is not. The extension needs exactly one library, and
# it is named libc.so — a name no file carries anywhere the loader looks.
assert elf_needed_libraries(extension) == ["libc.so"]
assert libc["libc_so_file"] is False, libc
assert libc["musl_libc_file"] is True, libc
# It imports regardless, because musl's loader answers to that name itself,
# and says so when asked directly. That name is the second thing that stops
# this file on glibc, behind the EXT_SUFFIX mismatch above: pushed past
# import with ctypes there, python:3.12-slim raises "libc.so: cannot open
# shared object file", carrying only libc.so.6. Measured on that image, not
# in here — nothing in this container can see it.
assert orjson.dumps({"loaded": True}) == b'{"loaded":true}'
report = musl_loader_report(extension)
assert "libc.so => /lib/ld-musl-x86_64.so.1" in report, report
# Run standalone the loader also cannot relocate the CPython symbols, which
# is the other half of how the extension is linked: Py* comes from the
# interpreter process, so nothing links libpython.
assert "Error relocating" in report and "PyDict_New: symbol not found" in report
assert not any("python" in name for name in elf_needed_libraries(extension))
# The reader is not just returning what suits the story: the same parser
# finds the versioned musl soname and libpython in the interpreter binary.
control = elf_needed_libraries(os.path.realpath(sys.executable))
assert "libc.musl-x86_64.so.1" in control, control
assert "libpython3.12.so.1.0" in control, control
# And the musl match is real rather than an image name: the ABI tag names
# musl, the musl loader is present, glibc's is not.
assert libc["soabi"] == "cpython-312-x86_64-linux-musl", libc
assert libc["host"] == "x86_64-pc-linux-musl", libc
assert libc["musl_loader"] is True and libc["glibc_loader"] is False, libc
# --- bytes, not str ------------------------------------------------------
# The difference that breaks the most call sites: the result never goes
# through a str decode on the way out.
assert isinstance(orjson.dumps({"a": 1}), bytes)
assert isinstance(json.dumps({"a": 1}), str)
# Two smaller output differences that break byte-for-byte comparisons
# against json: orjson emits no spaces after ":" or ",", and it has no
# ensure_ascii, so non-ASCII stays UTF-8 instead of being \uXXXX escaped.
assert orjson.dumps({"a": 1, "b": 2}) == b'{"a":1,"b":2}'
assert json.dumps({"a": 1, "b": 2}) == '{"a": 1, "b": 2}'
assert orjson.dumps({"k": "café"}) == '{"k":"café"}'.encode("utf-8")
assert json.dumps({"k": "café"}) == r'{"k": "caf\u00e9"}'
assert dumps_text({"k": "café"}) == '{"k":"café"}'
# The tight spacing is the default, not a fixed format: under OPT_INDENT_2
# the space after the colon comes back and the output is byte-for-byte what
# json.dumps(indent=2) produces. If a diff against json output has to keep
# passing, pretty printing is the cheaper migration than a separators fix.
pretty = {"a": [1, 2], "b": {"c": 1}}
assert dumps_text(pretty, indent=True) == json.dumps(pretty, indent=2)
assert dumps_text(pretty, indent=True) == '{\n "a": [\n 1,\n 2\n ],\n "b": {\n "c": 1\n }\n}'
# There is no file API at all — json.dump(obj, fp) has no counterpart, you
# write the bytes yourself.
assert not hasattr(orjson, "dump") and not hasattr(orjson, "load")
# --- datetime, date, UUID ------------------------------------------------
moment = dt.datetime(2026, 8, 14, 12, 30, 45, 123456, tzinfo=dt.timezone.utc)
identifier = uuid.UUID("12345678-1234-5678-1234-567812345678")
# orjson serializes these natively, in RFC 3339 form.
assert orjson.dumps({"t": moment}) == b'{"t":"2026-08-14T12:30:45.123456+00:00"}'
assert orjson.dumps(dt.date(2026, 8, 14)) == b'"2026-08-14"'
assert orjson.dumps(dt.time(12, 30, 45)) == b'"12:30:45"'
assert orjson.dumps({"u": identifier}) == b'{"u":"12345678-1234-5678-1234-567812345678"}'
# The offset is written +00:00, not Z. Consumers that match on Z need the
# option; this is the one datetime detail that bites after the swap.
assert orjson.dumps(moment, None, orjson.OPT_UTC_Z) == b'"2026-08-14T12:30:45.123456Z"'
# json raises for all of them, which is why so much code carries a default
# hook that orjson does not need.
assert "datetime is not JSON serializable" in str(
raises(TypeError, lambda: json.dumps({"t": moment}))
)
assert "UUID is not JSON serializable" in str(
raises(TypeError, lambda: json.dumps({"u": identifier}))
)
class Money:
def __init__(self, cents: int) -> None:
self.cents = cents
def encode_money(value):
if isinstance(value, Money):
return value.cents
raise TypeError(f"unsupported: {type(value).__name__}")
# --- the `default` argument, the other way round from the folklore --------
# Measured, against the expectation: orjson is the library that DOES take
# default positionally — dumps(obj, default=None, option=None) — and
# json.dumps is the one that refuses, because everything after obj there is
# keyword-only. So a positional hook that works with orjson raises when the
# call is switched back to json, not the reverse.
assert orjson.dumps({"m": Money(500)}, encode_money) == b'{"m":500}'
assert orjson.dumps({"m": Money(500)}, default=encode_money) == b'{"m":500}'
assert "takes 1 positional argument" in str(
raises(TypeError, lambda: json.dumps({"m": Money(500)}, encode_money))
)
assert json.dumps({"m": Money(500)}, default=encode_money) == '{"m": 500}'
# What orjson refuses is every other json.dumps keyword: obj, default and
# option are the entire signature.
assert "unexpected keyword argument" in str(
raises(TypeError, lambda: orjson.dumps({"b": 1}, sort_keys=True))
)
assert "unexpected keyword argument" in str(
raises(TypeError, lambda: orjson.dumps({"b": 1}, indent=2))
)
# Encode failures come back as builtins.TypeError itself: JSONEncodeError is
# an alias, not a subclass, so `except orjson.JSONEncodeError` and
# `except TypeError` are the same clause. Decode failures are a real
# subclass of both ValueError and json.JSONDecodeError, so json-shaped read
# error handling keeps working.
assert orjson.JSONEncodeError is TypeError
assert "Type is not JSON serializable: Money" == str(
raises(TypeError, lambda: orjson.dumps({"m": Money(500)}))
)
assert orjson.JSONDecodeError is not json.JSONDecodeError
assert issubclass(orjson.JSONDecodeError, json.JSONDecodeError)
assert issubclass(orjson.JSONDecodeError, ValueError)
# --- sort_keys is an option bit -----------------------------------------
messy = {"b": 1, "a": 2, "C": 3}
assert orjson.dumps(messy) == b'{"b":1,"a":2,"C":3}'
assert orjson.dumps(messy, None, orjson.OPT_SORT_KEYS) == b'{"C":3,"a":2,"b":1}'
assert dumps_text(messy, sort_keys=True) == '{"C":3,"a":2,"b":1}'
# Same ordering json produces with sort_keys=True — sorted by code point, so
# uppercase first. Only the spelling of the flag changed.
assert json.dumps(messy, sort_keys=True) == '{"C": 3, "a": 2, "b": 1}'
# --- non-str dict keys: refused, not coerced -----------------------------
assert "Dict key must be str" == str(raises(TypeError, lambda: orjson.dumps({1: "a"})))
assert orjson.dumps({1: "a"}, None, orjson.OPT_NON_STR_KEYS) == b'{"1":"a"}'
assert json.dumps({1: "a"}) == '{"1": "a"}'
# Why the refusal is worth having. json's silent coercion turns two distinct
# Python keys into one JSON key, emits both, and the value of the first is
# gone as soon as anything parses it back.
collision = {1: "a", "1": "b"}
assert len(collision) == 2
assert json.dumps(collision) == '{"1": "a", "1": "b"}'
assert json.loads(json.dumps(collision)) == {"1": "b"}
raises(TypeError, lambda: orjson.dumps(collision))
# OPT_NON_STR_KEYS opts into json's behaviour, collision included — the
# option is not a safer coercion, it is the same one made explicit.
assert orjson.dumps(collision, None, orjson.OPT_NON_STR_KEYS) == b'{"1":"a","1":"b"}'
# Both draw the line at keys that are not scalars.
raises(TypeError, lambda: orjson.dumps({(1, 2): "x"}, None, orjson.OPT_NON_STR_KEYS))
raises(TypeError, lambda: json.dumps({(1, 2): "x"}))
# --- nan and infinity: also the other way round --------------------------
hook_calls = []
def record(value):
hook_calls.append(value)
return None
# Measured, against the expectation: orjson does not reject non-finite
# floats. It writes null, silently, and the default hook is never consulted
# because float is a type orjson handles — so there is no way to intercept
# the loss from inside the dumps call.
assert orjson.dumps(float("nan"), record) == b"null"
assert orjson.dumps([float("inf"), float("-inf")], record) == b"[null,null]"
assert hook_calls == []
# json goes wrong in the opposite direction: it emits bare NaN and Infinity,
# which are not JSON, and only refuses when asked to.
assert json.dumps(float("nan")) == "NaN"
assert json.dumps([float("inf")]) == "[Infinity]"
assert "not JSON compliant" in str(
raises(ValueError, lambda: json.dumps(float("nan"), allow_nan=False))
)
# The consequence of that pairing: json output can be unreadable by orjson.
# json.loads takes its own non-standard tokens back, orjson does not.
broken = json.dumps({"x": float("inf")})
decode_error = raises(orjson.JSONDecodeError, lambda: orjson.loads(broken))
assert isinstance(decode_error, json.JSONDecodeError)
assert json.loads(broken)["x"] == float("inf")
# Since orjson cannot report the loss, the guard has to run before dumps.
assert has_non_finite_float({"a": [1.0, float("nan")]}) is True
assert has_non_finite_float({"a": [1.0, 2, "3"]}) is False
# Decimal is the other type that can hold a NaN, and the guard deliberately
# ignores it: orjson does not serialize Decimal at all, so a non-finite one
# raises instead of vanishing. The silence returns only if a default hook
# floats it, and then it is null like any other nan.
assert has_non_finite_float(Decimal("NaN")) is False
assert Decimal("NaN").is_nan() and Decimal("Infinity").is_infinite()
assert "Type is not JSON serializable: decimal.Decimal" == str(
raises(TypeError, lambda: orjson.dumps(Decimal("NaN")))
)
assert orjson.dumps({"d": Decimal("NaN")}, float) == b'{"d":null}'
# --- one more silent difference in range ---------------------------------
# orjson caps integers at 64 bits, json does not. Measured, the accepted
# band is the union of the signed and unsigned ranges — -2**63 through
# 2**64-1 — so an id at the very top of a uint64 column still serializes.
# What starts raising after the swap is a Python bignum, in either
# direction.
assert json.dumps(2**64) == "18446744073709551616"
assert orjson.dumps(2**64 - 1) == b"18446744073709551615"
assert orjson.dumps(-(2**63)) == b"-9223372036854775808"
assert "Integer exceeds 64-bit range" == str(raises(TypeError, lambda: orjson.dumps(2**64)))
assert "Integer exceeds 64-bit range" == str(
raises(TypeError, lambda: orjson.dumps(-(2**63) - 1))
)
print("contract ok:", orjson.__version__, info["tags"][0])