orjson
orjson is a JSON encoder and decoder for Python written in Rust. It exposes two functions, dumps() and loads(), and that is nearly the whole API. The trade for that small surface is speed and strictness: the README's own benchmarks put dumps() around ten times faster than the standard library and loads() around twice as fast, and it rejects invalid JSON the standard library happily accepts. It also serializes types json cannot touch without help, including dataclasses, datetime, date, time, UUID, and numpy arrays. The one thing that surprises every newcomer is that dumps() returns bytes, not str, because that is what you actually write to a socket or a file.
The fastest and strictest JSON library for CPython, and the right default for high-throughput services that already work in bytes. Skip it if you need json's keyword arguments, PyPy, or the ability to report a bug to a maintainer.
Use it if
- You serialize a lot of JSON per second in an API or a queue worker and encoding shows up in your profiles
- You serialize dataclasses, datetimes, UUIDs, or numpy arrays and are tired of writing a default= hook for each of them
- You want strict RFC 8259 behaviour: the README shows it rejecting 17 invalid fixtures the standard library accepts, and it refuses NaN and Infinity on input
- You write bytes anyway, to a socket, a file opened in binary mode, or a Redis value, so the bytes return type costs you nothing
- You want a drop-in replacement for json: there is no object_hook, no cls=, no ensure_ascii, no separators, indent is fixed at two spaces, and dumps returns bytes, so search-and-replace breaks in a dozen small ways
- You need typed decoding or validation: orjson deliberately will not deserialize into dataclasses, Decimals, or UUIDs, and says so in its FAQ; msgspec does typed decode and often beats orjson on the round trip
- You run PyPy, an embedded build for Android or iOS, or subinterpreters: the project states it does not and will not support those, and there is no pure-Python fallback if no wheel matches
- You need to file a bug or send a patch: the project runs with issues and pull requests turned off on purpose, so your options for an edge case are to fork it or wait
- Your JSON volume is modest: the standard library is already there, understood by everyone, and one fewer compiled dependency in your image
Setup reality
pip install orjson normally pulls a prebuilt wheel and needs no compiler. Wheels cover CPython 3.10 through 3.15 on Linux x86_64, aarch64, armv7, ppc64le and s390x, macOS x86_64 and arm64, and Windows x86, x86_64 and aarch64. Python 3.9 and earlier are out, and PyPy is never supported. Off the wheel path you build from source, which needs Rust 1.95, a C compiler, and maturin, which is a lot for a JSON library in a locked-down CI image. The migration work is the real cost: dumps() gives you bytes so anything expecting str needs .decode(), pretty printing is only OPT_INDENT_2, and every knob you used to pass as a keyword is now a bit flag masked together in option=.
Patterns
Serialize and parsebasic-dumps-loads
import orjson
blob = orjson.dumps({"id": 1, "name": "ada"})
print(blob) # b'{"id":1,"name":"ada"}'
data = orjson.loads(blob)
print(data["name"]) # adadumps returns bytes. If a caller needs str, call .decode() explicitly and accept that you just paid for a copy the library was trying to save you.
Serialize dataclasses with no default hookserialize-dataclass
from dataclasses import dataclass
import orjson
@dataclass
class User:
id: int
email: str
orjson.dumps([User(1, "a@example.com"), User(2, "b@example.com")])
# b'[{"id":1,"email":"a@example.com"},{"id":2,"email":"b@example.com"}]'Works for frozen dataclasses, subclasses, and ones using __slots__, though the README notes __slots__ is the slower path. Field names go out as-is; there is no rename or alias support.
Make naive datetimes serialize as UTCdatetime-naive-utc
import orjson, datetime
value = {"created_at": datetime.datetime(1970, 1, 1)}
orjson.dumps(value)
# b'{"created_at":"1970-01-01T00:00:00"}' no offset
orjson.dumps(value, option=orjson.OPT_NAIVE_UTC)
# b'{"created_at":"1970-01-01T00:00:00+00:00"}'Without OPT_NAIVE_UTC a naive datetime is written with no offset, and whatever reads it downstream gets to guess the timezone. Add OPT_UTC_Z if the consumer expects a trailing Z instead of +00:00.
Support a type orjson does not knowdefault-callback
import orjson, decimal
def default(obj):
if isinstance(obj, decimal.Decimal):
return str(obj)
raise TypeError
orjson.dumps({"price": decimal.Decimal("19.99")}, default=default)
# b'{"price":"19.99"}'Always raise on the unhandled branch. A default that falls off the end returns None implicitly, and that null gets serialized as a legitimate value with no error anywhere.
Know that NaN and Infinity turn into nullnan-becomes-null
import orjson
orjson.dumps([float("nan"), float("inf")])
# b'[null,null]'
orjson.loads(b'[NaN]')
# raises orjson.JSONDecodeErrorNeither value is valid JSON, so orjson writes null and refuses to read them back, while the standard library writes and reads NaN. Check for non-finite floats before serializing if silent nulls would corrupt your data.
Pretty-print outputpretty-print
import orjson
print(orjson.dumps(
{"a": [1, 2]},
option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS,
).decode())Two spaces is the only indent width offered. Options are bit flags masked together with |, not keyword arguments, and OPT_SORT_KEYS costs measurable time on large documents.
Serialize dicts with int or UUID keysnon-string-keys
import orjson, uuid
orjson.dumps({1: "a", 2: "b"})
# raises JSONEncodeError: Dict key must be str
orjson.dumps({1: "a"}, option=orjson.OPT_NON_STR_KEYS)
# b'{"1":"a"}'Off by default so you notice the coercion rather than silently shipping stringified keys. Turning it on also disables the map-key cache, which slows serialization of key-heavy documents.
Serialize numpy arrays directlynumpy-arrays
import orjson, numpy
orjson.dumps(
{"matrix": numpy.array([[1, 2], [3, 4]])},
option=orjson.OPT_SERIALIZE_NUMPY,
)
# b'{"matrix":[[1,2],[3,4]]}'Only contiguous arrays of supported dtypes take the fast path; anything else falls through to default= or raises. numpy scalars such as numpy.float64 also need this flag.
Write line-delimited JSON efficientlywrite-jsonl
import orjson
with open("events.jsonl", "wb") as fh:
for event in events:
fh.write(orjson.dumps(event, option=orjson.OPT_APPEND_NEWLINE))Open the file in binary mode; passing bytes to a text-mode handle raises TypeError. OPT_APPEND_NEWLINE exists because dumps(...) + b'\n' copies the whole immutable buffer for one byte.
Embed already-serialized JSON without re-parsingembed-cached-json
import orjson
cached = redis.get("user:1:profile") # already valid JSON bytes
orjson.dumps({
"ok": True,
"profile": orjson.Fragment(cached),
})Fragment splices the bytes in verbatim, skipping a loads-then-dumps round trip. It does no validation beyond a UTF-8 check on str input, so garbage in means invalid JSON out.
Use orjson for FastAPI responsesfastapi-response-class
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
app = FastAPI(default_response_class=ORJSONResponse)
@app.get("/items")
async def items():
return {"items": [1, 2, 3]}This only speeds up the encode step. Pydantic model validation and FastAPI's response_model conversion still run first and are usually the larger cost.
Parse bytes directly instead of decoding firstparse-bytes-not-str
import orjson
# slower: builds a throwaway str
data = orjson.loads(response.content.decode("utf-8"))
# faster: bytes, bytearray, and memoryview are accepted as-is
data = orjson.loads(response.content)The README calls this out explicitly: passing the original buffer keeps both memory use and latency lower. The input must be valid UTF-8 or you get a JSONDecodeError.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| msgspec | PyPI | You want decoding straight into typed structs with validation, or JSON and MessagePack from one library |
| ujson | PyPI | You want a faster json that still returns str and keeps most keyword arguments, and you can live with looser correctness |
| python-rapidjson | PyPI | You need extras orjson refuses, such as configurable number handling, datetime modes, and a JSON Schema validator |