orjson is a low-latency JSON library with native support for common types such as dataclasses, datetimes, and UUIDs. It is not a drop-in decision: its contract differs from json, and performance should be measured in the complete workload.

The essential difference: bytes

python -m pip install orjson
from datetime import datetime, timezone
import orjson

data = {"event": "login", "at": datetime.now(timezone.utc)}
content: bytes = orjson.dumps(data, option=orjson.OPT_UTC_Z)
decoded = orjson.loads(content)

dumps() returns bytes, not str. That suits HTTP bodies and binary files but can break code that concatenates text. Avoid unnecessary .decode() and .encode() cycles because they reduce the benefit.

Explicit types and options

For custom types, provide default and raise TypeError when conversion is unsupported:

from decimal import Decimal


def default(obj):
    if isinstance(obj, Decimal):
        return str(obj)
    raise TypeError


payload = orjson.dumps({"total": Decimal("19.90")}, default=default)

Serializing Decimal as a string is an API decision. Document it and test round trips. The Python JSON guide explains format limitations, while the dataclasses guide covers models orjson can serialize natively.

Migrate safely

Build compatibility tests from real payloads: Unicode, datetimes, large integers, non-string keys, and non-finite numbers. Compare correctness before speed. A useful benchmark includes reading, validation, serialization, and transport with representative data and repeated runs.

The official orjson documentation, accessed July 22, 2026, describes supported types, options, and migration differences. Keep json when it meets the requirement; choose orjson when measurements justify the dependency and the bytes contract is intentional.

Read and write at the right boundary

orjson.loads() accepts UTF-8 bytes, bytearray, memoryview, and text. It returns ordinary JSON types and does not reconstruct dataclasses, dates, or UUIDs automatically. Perform that validation and reconstruction in a separate layer.

from pathlib import Path
import orjson


def save_event(path: Path, event: dict) -> None:
    path.write_bytes(orjson.dumps(event))


def load_event(path: Path) -> dict:
    value = orjson.loads(path.read_bytes())
    if not isinstance(value, dict):
        raise ValueError("document must be a JSON object")
    return value

For HTTP APIs, check the framework contract. Some response types accept bytes; others expect a Python object and serialize it themselves. Serializing twice produces a JSON string containing escaped JSON.

Datetimes and timezone policy

orjson handles datetime, date, and time, but representation depends on the value and options:

from datetime import datetime, timezone
import orjson

event = {"created_at": datetime(2026, 8, 21, 15, 0, tzinfo=timezone.utc)}
payload = orjson.dumps(event, option=orjson.OPT_UTC_Z)
assert b"2026-08-21T15:00:00Z" in payload

OPT_NAIVE_UTC treats naive datetimes as UTC. Enable it only when the system guarantees that meaning; it can otherwise disguise local time with missing timezone information. OPT_OMIT_MICROSECONDS removes precision and is also a contract decision.

Dataclasses, enums, and UUIDs

Native dataclass support is convenient, but every serializable field may enter the output. That does not make an internal object a safe public API. A newly added operational field could unexpectedly become visible.

Build an explicit dictionary when stable names, versioning, or secret exclusion matter. Enums and UUIDs have documented representations; retain contract tests for the output consumers expect. The default callback handles unsupported values and is not a universal interception hook for every native type.

Keys and deterministic output

JSON object keys are strings. OPT_NON_STR_KEYS converts certain non-string keys, but distinct Python keys may collide after textual conversion. Prefer explicit normalization and validation.

OPT_SORT_KEYS creates deterministic ordering for snapshots or human comparison at an additional cost. Object order must not carry application semantics. Cryptographic signing or canonicalization requires the relevant specification; sorting keys alone does not guarantee canonical JSON.

Integers, floats, and interoperability

Python integers can grow arbitrarily large, while many consumers have limited numeric precision. OPT_STRICT_INTEGER can reject integers outside orjson's documented interoperable range. Decide whether identifiers are JSON numbers or strings.

Non-finite floats such as NaN and infinity are not standard JSON values. Do not assume orjson shares the standard module's policy. Include them in migration tests and validate scientific data before publishing it.

Invalid input raises orjson.JSONDecodeError; unsupported output raises JSONEncodeError. Catch errors only where you can add useful context or map them to a protocol response. Avoid hiding the cause with except Exception.

Formatting and logs

Compact output is appropriate for network and storage. OPT_INDENT_2 is available for human-maintained files, while OPT_APPEND_NEWLINE is useful for one-document-per-line output:

def json_line(record: dict) -> bytes:
    return orjson.dumps(record, option=orjson.OPT_APPEND_NEWLINE)

Fast serialization does not make full payload logging safe. Remove credentials, personal data, and tokens before logging. Enforce input and output size limits to control memory use.

Benchmark representative work

Measure with the Python version, hardware, options, and payload shapes used in production. Warm up the code, run several repetitions, and compare a median or distribution. Include encoding, decoding, and any necessary text conversion:

from timeit import repeat
import json
import orjson

data = [{"id": n, "active": True, "name": f"item-{n}"} for n in range(1000)]

json_times = repeat(lambda: json.dumps(data), number=100, repeat=5)
orjson_times = repeat(lambda: orjson.dumps(data), number=100, repeat=5)

print(min(json_times), min(orjson_times))

Do not present that ratio as universal. Payload, CPU, versions, and options change it. If database access, network latency, or validation dominates the request, faster serialization may not produce a meaningful user-facing gain.

Migration checklist

  • confirm whether every caller expects str or bytes;
  • compare Unicode, escaping, dates, decimals, UUIDs, and enums;
  • test large integers, non-finite floats, and non-string keys;
  • verify exception types relied on by the application;
  • inspect framework, cache, file, and queue integrations;
  • retain representative fixtures consumed by other services;
  • benchmark the complete path;
  • document options and compatibility policy.

A small adapter function can centralize options and error handling. It prevents endpoints from inventing conflicting policies and keeps a return to the standard module feasible.

Security and operational compatibility

Treat JSON as untrusted input. orjson parses a document, but it does not enforce a schema, authorization, or protocol limits. Restrict body size before loading, validate nesting and collection sizes according to the domain, and reject unknown fields when the contract requires it. Faster parsing does not remove denial-of-service risk.

When upgrading the dependency, read release notes and run compatibility fixtures. Binary wheels and platform requirements differ from a standard-library module, so confirm availability in the build and production environments. Production must be reproducible from declared dependencies rather than a one-off server compilation.

Separate benchmarks from functional tests. CI should verify output and error behavior deterministically; strict timing thresholds fluctuate on shared runners. Track performance in a controlled environment or allow broad regression thresholds with enough diagnostic context.

Make every option understandable in review. A mask containing several constants can live in a domain-named adapter with focused tests. Maintainers should know why UTC uses Z, whether microseconds are retained, how dataclasses are exposed, and which numeric ranges are permitted without reverse-engineering a bitwise expression.

Deciding whether to adopt it

orjson is a strong candidate when profiling shows meaningful JSON cost, a bytes API fits the transport, and its supported types match the contract. The standard json module remains a sound choice for small payloads, scripts, maximum portability, or code that depends heavily on its customization hooks.

Record the decision and a baseline measurement. Revisit it when payload shapes or architecture change. Serialization that mattered in an in-process API may become negligible after work moves behind a database or network boundary. A dependency earns its place through measured system value and a clear contract, not through an isolated headline benchmark.