Article

Comparing json and orjson in Python: Which JSON Library Should You Use in 2025?

by Gary Worthington, More Than Monkeys

If you’ve written any amount of Python backend code, you’ve almost certainly used the built-in json module. It works, so why change?

As your data grows or your API traffic spikes, the performance ceiling becomes obvious; serialising and deserialising large payloads gets slow, and milliseconds turns in seconds.

That’s when you might hear about orjson: a high-performance, Rust-powered alternative that promises significant speed gains while keeping a similar API.

In this article, I’ll compare json and orjson in the same way I compared requests and httpx with real examples, benchmarks, and the quirks that will trip you up if you switch.

TL;DR

  • Stick with json if you value built-in support, rock-solid compatibility, and don’t need extreme performance.
  • Use orjson if you’re moving large amounts of data or serving high-traffic APIs; the performance boost is real.
  • Be aware of stricter type handling, different defaults, and the fact it returns bytes instead of str.

The Basics: Serialising and Deserialising

Both libraries turn Python objects into JSON and back, but there are some immediate differences.

json (standard library)

import json

data = {"name": "Gary", "age": 42}
json_str: str = json.dumps(data)
restored = json.loads(json_str)
  • json.dumps() returns a string.
  • Works out of the box, no installation needed.

orjson (third-party)

import orjson

data = {"name": "Gary", "age": 42}
json_bytes: bytes = orjson.dumps(data)
restored = orjson.loads(json_bytes)

orjson.dumps() returns bytes, not a string.
If you need a string:

orjson.dumps(data).decode()

That return type is the first “gotcha” for anyone swapping it into an existing codebase.

Performance: The Numbers

Let’s benchmark serialising and deserialising a large object.

import timeit
import json
import orjson

obj: dict = {"users": [{"id": i, "name": f"user{i}"} for i in range(10_000)]}

print("json.dumps:", timeit.timeit(lambda: json.dumps(obj), number=10))
print("orjson.dumps:", timeit.timeit(lambda: orjson.dumps(obj), number=10))

json_str: str = json.dumps(obj)
orjson_bytes: bytes = orjson.dumps(obj)

print("json.loads:", timeit.timeit(lambda: json.loads(json_str), number=10))
print("orjson.loads:", timeit.timeit(lambda: orjson.loads(orjson_bytes), number=10))

MacBook Pro M1, Python 3.11 results:

json.dumps:   0.202s
orjson.dumps: 0.047s
json.loads:   0.158s
orjson.loads: 0.045s

For large data, orjson is 4–5× faster. If your API is CPU-bound, that’s a meaningful saving.

Behaviour Differences You Need to Know

The API feels familiar, but orjson has opinions.

1. Return Types Matter

  • json.dumps() → str
  • orjson.dumps() → bytes

This can cause subtle bugs in tests and responses.

In FastAPI, for example:

from fastapi.responses import ORJSONResponse

@app.get("/users")
def get_users():
return ORJSONResponse(content={"users": [...]})

If you return orjson.dumps(obj) directly, you’ll likely break encoding.

2. Datetime Handling

json can’t serialise datetime objects without a helper:

from datetime import datetime
import json

json.dumps({"now": datetime.utcnow()})
# TypeError: Object of type datetime is not JSON serializable

You fix it with:

json.dumps({"now": datetime.utcnow()}, default=str)

orjson supports datetimes out of the box, producing ISO 8601 with millisecond precision:

import orjson
from datetime import datetime

orjson.dumps({"now": datetime.utcnow()})
# b'{"now":"2025-08-08T09:59:00.123Z"}'

That alone can remove a lot of boilerplate in APIs.

3. Strict Type Handling

orjson enforces more rules than json. One example is Decimal:

from decimal import Decimal
import json, orjson

json.dumps({"price": Decimal("9.99")}) # Works, casts to float
orjson.dumps({"price": Decimal("9.99")}) # Raises TypeError

To make it work in orjson, you can pass a default callable, which allows you to override the behaviour (similar to json)

from decimal import Decimal
import orjson

def default(obj):
if isinstance(obj, Decimal):
return str(obj) # Or float(obj) if you prefer
raise TypeError

orjson.dumps({"price": Decimal("9.99")}, default=default)
# b'{"price":"9.99"}'

This keeps control over how special types are serialised, rather than letting them be silently coerced.

4. Options and Flags

You can also tweak orjson behaviour with bitwise flags:

orjson.dumps(
data,
option=orjson.OPT_SORT_KEYS | orjson.OPT_NAIVE_UTC
)

This gives you precision over encoding, sorting, and timezone handling without writing your own encoders.

Testing and Mocking

If your code returns raw orjson.dumps() output, tests will need to deal with bytes.

You could decode in the function itself:

def to_json(data: dict) -> str:
return orjson.dumps(data).decode()

Then in tests:

def test_to_json():
assert to_json({"foo": "bar"}) == '{"foo":"bar"}'

Better still; test the structure, not string formatting:

import json

def test_to_json():
result = to_json({"foo": "bar"})
assert json.loads(result) == {"foo": "bar"}

This avoids brittle tests that break due to whitespace or key order.

Which One Should You Choose?

  • Use json if:
  • You value simplicity, portability, and no external dependencies.
  • Your performance needs are modest.
  • Use orjson if:
  • You’re building high-throughput APIs.
  • You work with large or frequent JSON payloads.
  • You’re comfortable with stricter type handling and minor API differences.

In most backend APIs I’ve built recently, orjson wins. The performance gain is worth adapting to its quirks, and in frameworks like FastAPI the integration is seamless.

Links

Gary Worthington is a software engineer, delivery consultant, and agile coach who helps teams move fast, learn faster, and scale when it matters. He writes about modern engineering, product thinking, and helping teams ship things that matter.

Through his consultancy, More Than Monkeys, Gary helps startups and scaleups improve how they build software — from tech strategy and agile delivery to product validation and team development.

Visit morethanmonkeys.co.uk to learn how we can help you build better, faster.

Follow Gary on LinkedIn for practical insights into engineering leadership, agile delivery, and team performance