orjson: a JSON library for Python that returns bytes and knows datetimes
Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy
At a glance
- What is it?
- orjson is a CPython extension written in Rust that serializes dataclasses, datetimes, UUIDs and numpy arrays without a default hook. It is fast, but it changes the shape of the json API in ways that break drop-in replacement.
- Who is it for?
- Adopt orjson when the payload contains datetimes, UUIDs, dataclasses or numpy arrays and you control the call sites, because the default hook you would otherwise write disappears. Do not adopt it as a silent swap for json in a codebase that asserts on str output, passes sort_keys or indent, or relies on non-str dict keys.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem orjson removes: the default hook you keep rewriting
The standard library json module serializes a fixed set of Python types. Anything else, a datetime, a UUID, a dataclass instance, a numpy array, raises TypeError unless you supply a default callable. In practice that callable gets copied between services, drifts, and turns into a small internal library. orjson's answer is to serialize those types natively, so the hook is unnecessary for the common cases.
The README lists the natively serialized set: str, dict, list, tuple, int, float, bool, None, dataclasses.dataclass, typing.TypedDict, datetime.datetime, datetime.date, datetime.time, uuid.UUID, numpy.ndarray, and orjson.Fragment. Subclasses of str, int, dict and list are serialized too, which the migration notes call out as a change from version 2 and as closer to standard library behaviour.
The audience is narrow and identifiable. If your service boundary is JSON and your objects are Python-native, orjson removes code. If your payloads are already plain dicts of strings and numbers, the gain is throughput and nothing else, and the migration cost may not be worth paying.
Rust core, Python surface: what dumps and loads actually do
The package is a CPython extension. Cargo.toml declares crate-type cdylib, the build backend is maturin, and the Python source lives under pysrc with Rust under src. There is a vendored yyjson include directory and simdutf8 among the dependencies, which is consistent with the README's claim that loads is strictly compliant with UTF-8 and RFC 8259.
The public surface is two functions. dumps takes the object, an optional default callable and an optional integer option bitmask, and returns bytes. loads takes bytes and returns Python objects. That is the whole data flow: Python object graph in, UTF-8 bytes out, and the reverse.
Two design decisions follow from that shape. First, dumps returns bytes rather than str, which the migration section names as the largest difference from the standard library. Second, feature toggles that json exposes as arguments become flags in a single option integer. OPT_SORT_KEYS replaces sort_keys, OPT_INDENT_2 replaces indent, and the README states that other indentation levels are not supported. OPT_NON_STR_KEYS exists for dict objects with non-str keys. OPT_PASSTHROUGH_SUBCLASS and OPT_PASSTHROUGH_DATACLASS opt back out of the native handling.
The README is explicit that file reading and writing, line-delimited JSON files and similar I/O are not provided. orjson serializes and deserializes in memory and stops there.
Installing orjson and serializing a datetime and a numpy array
The README says to install a wheel from PyPI, installing the orjson package. In requirements.txt format it gives a bounded range, and in pyproject.toml format a caret range. Prebuilt wheels are published for Linux (amd64, i686, aarch64, armv7), macOS (amd64, aarch64) and Windows (amd64, i686, aarch64).
pip install orjsonThat pulls a wheel for your platform if one exists. The README notes that amd64 wheels published to PyPI run on x86-64-v1 (2003) or later and use AVX-512 at runtime when the CPU provides it; aarch64 wheels require ARMv8-A (2011) or later. If your interpreter or architecture is not in that list, there is no wheel and you are building from source with maturin, which requires a Rust toolchain.
The quickstart in the README combines options with a bitwise OR. The example builds a dict containing a datetime, a string with a non-ASCII character and a numpy array, then serializes with OPT_NAIVE_UTC and OPT_SERIALIZE_NUMPY.
>>> import orjson, datetime, numpy
>>> data = {
... "type": "job",
... "created_at": datetime.datetime(1970, 1, 1),
... "status": "\U0001f197",
... "payload": numpy.array([[1, 2], [3, 4]]),
... }
>>> orjson.dumps(data, option=orjson.OPT_NAIVE_UTC | orjson.OPT_SERIALIZE_NUMPY)
b'{"type":"job","created_at":"1970-01-01T00:00:00+00:00","status":"\xf0\x9f\x86\x97","payload":[[1,2],[3,4]]}'The output is bytes, the naive datetime gains a +00:00 offset because of OPT_NAIVE_UTC, and the numpy array becomes a nested list. Feeding those bytes back to orjson.loads returns a dict in which created_at is now a plain string; the datetime type is not reconstructed on the way back. That asymmetry is the thing to internalize before wiring orjson into a round-trip path.
Where orjson is the wrong tool
The README states plainly that orjson does not and will not support PyPy, embedded Python builds for Android and iOS, or PEP 554 subinterpreters. If any of those is your runtime, the conversation is over. Free-threading under PEP 703 is described as something orjson may support when it is stable, which is a statement about the future rather than a supported configuration today.
The second limitation is the return type. Code that does json.dumps(obj) and then calls .replace, .encode, string concatenation or a regex on the result will fail on bytes. This shows up in logging helpers, templating and test assertions. The migration section flags it as the largest difference, and it is the one most likely to surface only at runtime.
The third is output control. OPT_INDENT_2 gives two-space indentation and the README says other levels are not supported, so a codebase that emits four-space pretty JSON for human review cannot reproduce that with orjson. The README also notes that ensure_ascii is probably not relevant today and that UTF-8 characters cannot be escaped to ASCII, so pipelines that require pure-ASCII output need a different tool.
Finally, orjson does not do I/O. If your problem is streaming a large line-delimited JSON file, orjson is one component, not the solution, and you still need to write the reading and writing loop yourself.
orjson against the standard library, ujson and msgspec
Against the standard library json, the difference is not only speed. json.dumps returns str, accepts sort_keys and indent as keyword arguments, and raises TypeError on a datetime until you write a default. orjson returns bytes, folds those keywords into an option bitmask, and serializes the datetime itself. The README claims dumps is something like 10x as fast as json and loads something like 2x, and it links to a Performance section and a Reproducing subsection rather than presenting the numbers inline. Treat those multipliers as the project's own claim and measure your payloads.
Against ujson, the difference is the type surface and the correctness posture. ujson is a C/C++ extension with its own history of edge-case handling; orjson is Rust, ships a vendored yyjson, and the README positions strict UTF-8 and RFC 8259 compliance as a differentiator against both the standard library and third-party libraries.
Against msgspec, the difference in approach is broader than serialization. msgspec pairs a serializer with schema-driven decoding into typed structs, so it takes over validation and the shape of your data model. orjson deliberately does not: loads returns plain dicts and lists, and there is no schema layer. If you want typed decoding, orjson is not the tool; if you want a fast encoder that stays out of your data model, msgspec's schema layer is the extra machinery you would be carrying.
Against pydantic, the comparison is not like for like. pydantic is a validation framework that needs a JSON encoder for output; orjson is that encoder. They compose rather than compete, and the question is whether pydantic's own serialization path is fast enough for your case.
Licence, release cadence and the cost of upgrading
The licence is not a single identifier. pyproject.toml declares license as MPL-2.0 AND (Apache-2.0 OR MIT), the classifiers list all three, and the repository root carries LICENSE-MPL-2.0, LICENSE-APACHE and LICENSE-MIT. The README says the source contains code under the Mozilla Public License 2.0, Apache 2.0 and MIT. MPL-2.0 is file-level copyleft, which is a different obligation from a permissive-only dependency. Whether that matters depends on how you distribute the package and on your own policy; this is a description of what the files say, not legal advice, and a licence review is the right place to settle it.
Upgrades are governed by a stated policy: releases follow semantic versioning, and serializing a new object type without an opt-in flag counts as a breaking change. That is a useful commitment because it means a minor or patch bump should not start serializing something that previously raised or went through your default hook. The migration notes for version 3 describe exactly that class of change: subclasses of str, int, dict and list became serialized by default, dataclass instances became serialized by default, and uuid.UUID became serialized by default. The notes also say the old default-function implementations and enabling options can be removed but do not need to be, which softens the upgrade.
The maintenance signal is concrete. The last push to the default branch was on 2026-09-14, and 3.12.0 was released on 2026-08-14. There is no open issue tracker or pull request queue, which the README attributes to signal-to-noise ratio. That removes a channel you might otherwise use to report a bug or check whether yours is known; the CHANGELOG in the repository is the record you have, and the README's Questions section is where the project points for support. Budget for that when you plan an upgrade: you are reading a changelog, not a thread.
Editorial conclusion
Adopt orjson when the payload contains datetimes, UUIDs, dataclasses or numpy arrays and you control the call sites, because the default hook you would otherwise write disappears. Do not adopt it as a silent swap for json in a codebase that asserts on str output, passes sort_keys or indent, or relies on non-str dict keys. Before committing, verify three things on your own interpreter: that a wheel exists for your platform and CPython version, that every call site tolerates bytes from dumps(), and that dict keys in your payloads are strings unless you pass OPT_NON_STR_KEYS.
Frequently asked questions
Is orjson faster than the standard json module in Python?
The README states that orjson.dumps is something like 10x as fast as json and orjson.loads something like 2x as fast, and links to a Performance section with a Reproducing subsection. Those are the project's own figures, so benchmark your own payloads before relying on them.
What is orjson?
It is a JSON library for Python that serializes dataclass, datetime, numpy and UUID instances natively. It is distributed as a CPython extension written in Rust, with dumps returning bytes and loads returning Python objects.
How do I install orjson?
The README says to install a wheel from PyPI by installing the orjson package, and gives a bounded version range for requirements files and a caret range for pyproject.toml. Prebuilt wheels are published for Linux, macOS and Windows on the architectures listed in the README.
What are the differences between orjson and json in Python?
The migration section names the largest one: orjson.dumps returns bytes where json.dumps returns str. sort_keys becomes option=orjson.OPT_SORT_KEYS, indent becomes option=orjson.OPT_INDENT_2 with no other levels, and dict objects with non-str keys need option=orjson.OPT_NON_STR_KEYS.
How do I use orjson?
Import orjson and call orjson.dumps with an optional default callable and an optional option bitmask, or orjson.loads on bytes. The README quickstart combines two flags with a bitwise OR to serialize a dict holding a datetime and a numpy array.
Is orjson safe?
The README does not make a safety claim in those terms. It does state that orjson.loads is strictly compliant with UTF-8 and RFC 8259, and that the library does not and will not support PyPy, Android and iOS embedded builds, or PEP 554 subinterpreters.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ijl-orjson)