Python orjson vs json performance: API differences
python orjson vs json performance: Compare Python's standard json module with orjson: API differences, type support, and the performance tradeoffs that matter in produ...
When a Python service spends measurable time serializing and deserializing JSON, the python orjson vs json performance question usually comes up. orjson is faster in many workloads, but the decision involves more than speed: the two libraries differ in return types, options, type support, and parsing strictness. This article walks through those differences so you can decide whether switching is worth the changes to your code.
What Changes in the Serialization API
The most visible difference is the return type. json.dumps() returns a str, while orjson.dumps() returns bytes.
import json import orjson payload = {"name": "Ada", "role": "engineer"} standard = json.dumps(payload) # str fast = orjson.dumps(payload) # bytes print(type(standard)) # <class 'str'> print(type(fast)) # <class 'bytes'>
Code that writes JSON to a socket, a message queue, or a binary file often needs bytes anyway, so the change is natural there. Code that concatenates JSON with str values, logs it, or passes it to an API that expects str needs an explicit .decode("utf-8").
Options are passed differently as well. The standard library uses named parameters such as indent and sort_keys. orjson uses bit flags combined with the option argument.
# Standard library json.dumps(payload, indent=2, sort_keys=True) # orjson orjson.dumps(payload, option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS)
orjson does not support ensure_ascii. It always emits UTF-8. If a downstream system requires ASCII-escaped output, you must add that transformation yourself, which is a real compatibility constraint.
Why Serialization Performance Differs
The standard library json module is not pure Python. In CPython it uses C accelerators for the hot encoding and decoding paths, so the gap between json and orjson is smaller than comparing pure Python against native code. Still, orjson is implemented in Rust on top of the serde and serde_json crates, and it avoids several overheads the standard library carries.
orjson writes directly into a growable buffer with minimal intermediate allocations. It has specialized fast paths for dict, list, str, int, float, and bool, and it serializes datetime, UUID, dataclass, and numpy types natively instead of routing them through a default callable. The standard library has no native path for those types, so every such value goes through Python-level dispatch.
The exact speedup depends on the data shape, the Python version, and the platform. What is consistent is the mechanism: fewer allocations, native handling of more types, and less Python-level dispatch per value. If you profile and see json.dumps or json.loads consuming a meaningful share of CPU, orjson is a reasonable candidate to test.
Deserialization Differences
orjson.loads() accepts bytes or str and returns parsed data. The parsing behavior is stricter than the standard library in ways that can change what your code accepts.
json.loads() supports hooks such as object_hook, parse_float, and parse_int. orjson.loads() does not. If your code relies on those hooks to transform values during parsing, switching to orjson means moving that logic to a post-processing step.
orjson also rejects JSON input with duplicate keys, while json.loads() silently keeps the last occurrence. For most payloads this does not matter, but it can surface malformed data that previously passed silently.
Native Support for Types the Standard Library Cannot Handle
The standard library raises TypeError when you pass a datetime, UUID, dataclass, or numpy value to json.dumps() without a default callable. orjson serializes these natively.
from datetime import datetime, timezone import orjson fixed = datetime(2025, 1, 15, 10, 30, tzinfo=timezone.utc) print(orjson.dumps({"timestamp": fixed})) # b'{"timestamp":"2025-01-15T10:30:00+00:00"}'
With the standard library, the equivalent requires a default function that converts each unsupported type, and the conversion logic lives in your application. orjson removes that boilerplate and, because the conversion happens inside the native encoder, it avoids the per-value Python call overhead.
Error Handling and Strictness
orjson is deliberately stricter than json in a few areas, and that strictness can break code that worked silently before.
Non-string dictionary keys are a common case. json.dumps({1: "a"}) coerces the integer key to the string "1". orjson raises TypeError for keys that are not strings, integers, floats, or booleans unless you pass OPT_NON_STR_KEYS.
Floating-point special values behave differently too. json.dumps(float("nan")) emits the non-standard token NaN by default. orjson raises an error unless you pass OPT_SERIALIZE_NAN. The same applies to Infinity and -Infinity.
These differences are worth auditing before a migration, because they change the boundary of what your serializer accepts.
When to Choose orjson Over the Standard Library
Use orjson when profiling shows JSON serialization or parsing is a meaningful share of your CPU time, when you need native serialization for datetime, UUID, dataclass, or numpy values, or when you want stricter parsing that rejects duplicate keys and non-standard float tokens.
Stay with the standard library when you depend on object_hook, parse_float, parse_int, or ensure_ascii, when you need indent or sort_keys as parameters at many call sites, or when adding a compiled third-party dependency is not acceptable for your deployment or security constraints.
The two libraries can coexist. A common pattern is to keep json for internal tooling and configuration handling, and use orjson on the hot path where payloads are large or serialization happens frequently.
Operational Considerations
orjson is a compiled extension. It ships wheels for common platforms and Python versions, but in an environment without a matching wheel, building from source requires a Rust toolchain. That is a real constraint for locked-down build systems.
Because orjson is a third-party dependency, you inherit its release cadence and compatibility policy. The standard library is always present and always matches your Python version. For long-lived applications, that difference matters for maintenance.
The bytes return type is the other operational detail. Every call site that expects str needs a decode, and every consumer that reads JSON from orjson output must accept bytes. That is a small change, but it touches every place that calls the serializer, so it should be part of the migration plan rather than an afterthought.