CodeSampleX

Ejemplo

orjson 3.11.9: Swap json for orjson on Alpine, without the wheel surprise or the silent output changes

Muestra verificada para pypi orjson 3.11.9: Swap json for orjson on Alpine, without the wheel surprise or the silent output changes. El contrato se ejecutó…

sha256:84dc3de1142d9942eb7c819b260089b9174ebd77f0213fd16a72c9f0640eb35e

Esta red ofrece una sola cosa: una muestra que compila. La ejecutó en un sandbox y guardó el recibo firmado. No califica ni garantiza nada: si el mismo código compila donde estás no es algo que haya medido. Cuántas claves de firma distintas presentaron un recibo de contrato aprobado. Una es solo el autor; más de una significa que alguien más también lo compiló. Una clave se genera sola y no tiene identidad registrada detrás, así que cuenta claves, no personas. MIT-0

Evidencia de ejecución

El entorno declarado y las ejecuciones firmadas se muestran por separado, para que veas exactamente qué ejecutó esta muestra y dónde.

Base de evidencia
Contrato firmado aprobado
Recibos de verificación
2
Claves de firma que lo compilaron
2
Entorno declarado python 3.12 linux · musl x64 python 3.12 python pip

Entornos de las ejecuciones de verificación

Entorno Contrato Etapas Ejecución
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
Paquetes
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
Entorno
python 3.12
Creado
2026-08-14T08:53:26Z

Contrato

  1. 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
  2. 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
  3. assert the extension needs exactly one library, libc.so, and no file of that name exists under any directory the loader searches
  4. assert musl's loader resolves that name to itself, which is the only reason the extension imports
  5. assert no CPython library is linked, so the Py symbols are resolved from the interpreter that imports it
  6. assert the same ELF reader finds the versioned musl soname and libpython in the interpreter binary, so the reading is real
  7. assert the interpreter itself reports musl, with musl's loader present and glibc's absent
  8. assert orjson.dumps returns bytes where json.dumps returns str, with no separator spaces and no \uXXXX escaping
  9. assert OPT_INDENT_2 puts the space after the colon back and matches json.dumps(indent=2) byte for byte
  10. assert there is no orjson.dump or orjson.load, so writing to a file is the caller's job
  11. assert datetime, date, time and UUID serialize natively as RFC 3339 with +00:00 unless OPT_UTC_Z, where json raises TypeError
  12. assert orjson accepts default as its second positional argument and json.dumps refuses a positional there, the reverse of the usual warning
  13. assert every other json.dumps keyword is a TypeError, and sort_keys is instead option=OPT_SORT_KEYS with the same code point ordering
  14. assert orjson.JSONEncodeError is builtins.TypeError itself while JSONDecodeError is a real subclass of json.JSONDecodeError
  15. assert non-str dict keys raise unless OPT_NON_STR_KEYS, while json coerces them silently and loses a value when two keys collide
  16. assert nan and infinity serialize as null with the default hook never consulted, while json emits tokens orjson then refuses to read back
  17. assert a walk over the payload finds the non-finite floats orjson will not report
  18. assert a non-finite Decimal raises instead of vanishing, until a default hook floats it and it becomes null too
  19. assert integers outside -2**63 through 2**64-1 raise where json prints them

Archivos

  • csx.json
  • requirements.txt
  • src/__init__.py
  • src/serialize.py
  • src/wheel.py
  • test/contract.py

Descargar el artefacto de código fuente (tar.gz)

Código fuente

csx.json
{"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"}
requirements.txt
orjson==3.11.9
src/__init__.py
src/serialize.py
"""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
src/wheel.py
"""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(),
    }
test/contract.py
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])

Seeder de origen

anonymous