Python orjson dumps loads and serialization
python orjson dumps loads and serialization: Learn how to use orjson's dumps and loads for fast JSON serialization in Python, including options, dataclass support, dat...
When you need to serialize Python objects to JSON and parse JSON back into Python, the standard json module is often the default choice. But for high-throughput services, orjson provides a faster alternative with a similar API. This article focuses on python orjson dumps loads and serialization — how to use orjson.dumps() and orjson.loads() effectively, what options matter, and where the library behaves differently from the standard library.
Why Choose orjson Over the Standard json Module
orjson is a Rust-backed JSON library that offers significantly faster serialization and deserialization than json in many workloads. It also supports native serialization for common Python types like datetime, UUID, and dataclasses without custom converters. The API is intentionally close to json, so migrating is straightforward in most cases.
The main differences appear in the dumps() function's options and in how orjson handles non-serializable types. For example, orjson.dumps() returns bytes by default, not a str. That matters when you write to a file or send over a network. orjson.loads() accepts both str and bytes input, which is convenient.
Basic Usage: dumps and loads
Start with the simplest case: serialize a dictionary and parse it back.
import orjson data = {"name": "Ada", "roles": ["admin", "developer"]} serialized = orjson.dumps(data) print(serialized) # b'{"name":"Ada","roles":["admin","developer"]}' parsed = orjson.loads(serialized) print(parsed) # {'name': 'Ada', 'roles': ['admin', 'developer']}
Notice that dumps() returns a bytes object. If you need a str, call .decode() on the result. The output is compact: no extra spaces after commas or colons. This is intentional and often desirable for storage and transmission.
loads() accepts bytes, str, or bytearray. It raises JSONDecodeError on invalid input, just like json.loads(). The error message may differ, but the exception type is the same.
Controlling Output with OPT_* Flags
orjson.dumps() accepts an option parameter that combines flags from orjson.OPT_*. These flags change formatting or behavior. The most commonly used ones are:
| Flag | Effect |
|---|---|
OPT_INDENT_2 | Pretty-print with two spaces per indentation level |
OPT_SORT_KEYS | Sort dictionary keys alphabetically |
OPT_NAIVE_UTC | Treat naive datetime objects as UTC |
OPT_UTC_Z | Serialize UTC datetimes with a Z suffix instead of +00:00 |
OPT_SERIALIZE_NUMPY | Serialize NumPy arrays natively |
OPT_SERIALIZE_DATACLASS | Serialize dataclass instances without custom code |
OPT_SERIALIZE_UUID | Serialize UUID objects as strings |
OPT_OMIT_MICROSECONDS | Omit microseconds from datetime and time objects |
Combine flags with the bitwise OR operator |.
import orjson data = {"b": 1, "a": 2} pretty_sorted = orjson.dumps(data, option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS) print(pretty_sorted.decode())
Output:
{ "a": 2, "b": 1 }
Using OPT_SORT_KEYS can make output deterministic, which helps with caching and debugging. OPT_INDENT_2 is useful for logs or human-readable configuration files, but it increases payload size.
Serializing Dataclasses and Custom Objects
orjson can serialize dataclass instances natively if you pass OPT_SERIALIZE_DATACLASS. Without this flag, dumps() raises TypeError for dataclass objects.
import orjson from dataclasses import dataclass @dataclass class User: id: int name: str user = User(id=1, name="Ada") # Without the flag, this raises TypeError # orjson.dumps(user) serialized = orjson.dumps(user, option=orjson.OPT_SERIALIZE_DATACLASS) print(serialized) # b'{"id":1,"name":"Ada"}'
The flag also handles nested dataclasses, lists of dataclasses, and dataclasses containing other supported types. If you need to serialize arbitrary objects, you must provide a default function.
dumps() accepts a default callable that receives the object and returns a serializable representation. This is similar to json.dumps()'s default parameter.
import orjson from datetime import datetime def default(obj): if isinstance(obj, datetime): return obj.isoformat() raise TypeError now = datetime.now() serialized = orjson.dumps({"time": now}, default=default) print(serialized) # b'{"time":"2025-01-01T12:00:00"}'
Be careful: default is only called for types that orjson cannot handle natively. If you pass OPT_SERIALIZE_DATACLASS, dataclasses are handled before default is invoked.
Handling datetime and UUID Types
orjson has built-in support for datetime, date, time, and UUID objects, but the behavior depends on the flags you set.
For datetime objects, orjson serializes them as RFC 3339 strings. Naive datetimes (without timezone info) are serialized without a timezone offset. To treat naive datetimes as UTC, use OPT_NAIVE_UTC. To append Z instead of +00:00 for UTC datetimes, use OPT_UTC_Z.
import orjson from datetime import datetime, timezone utc_now = datetime.now(timezone.utc) print(orjson.dumps(utc_now)) # b'"2025-01-01T12:00:00+00:00"' print(orjson.dumps(utc_now, option=orjson.OPT_UTC_Z)) # b'"2025-01-01T12:00:00Z"'
For UUID, use OPT_SERIALIZE_UUID to serialize as a string. Without it, orjson raises TypeError.
import orjson from uuid import uuid4 uuid_obj = uuid4() print(orjson.dumps(uuid_obj, option=orjson.OPT_SERIALIZE_UUID)) # b'"..."'
These options give you control over output format without writing custom converters. If you need a different format, use the default function.
Performance and Memory Considerations
orjson is designed for speed, but its actual advantage depends on your data shape and workload. The library avoids Python-level loops for serialization and uses a Rust implementation that writes directly to a buffer. This reduces CPU usage and often lowers memory overhead compared to json.
However, you should not assume that orjson is always faster. For very small payloads, the overhead of importing the module and calling into Rust may be similar to json. The benefits become more visible with larger JSON documents or when serializing many objects in a tight loop.
One important operational detail: orjson.dumps() returns a bytes object that is immutable. If you need to modify the output, you must copy it. Also, orjson does not support streaming serialization like json.JSONEncoder.iterencode(). For very large data structures, you may need to chunk your data manually.
Memory usage is generally lower because orjson avoids creating intermediate Python objects for every nested value. Still, the output bytes object holds the entire serialized result in memory. For extremely large payloads, consider writing directly to a file or socket using a streaming approach, but that requires manual chunking.
Error Handling and Edge Cases
orjson.dumps() raises TypeError for unsupported types, and orjson.loads() raises JSONDecodeError for malformed JSON. The error messages from orjson are often more descriptive than those from json, but you should still handle them explicitly in production.
import orjson try: orjson.dumps({"key": object()}) except TypeError as e: print(f"Serialization failed: {e}") try: orjson.loads(b'{"invalid": true,') except orjson.JSONDecodeError as e: print(f"Parse failed: {e}")
Note that orjson.JSONDecodeError is a subclass of ValueError, so catching ValueError also works. When using default, ensure it raises TypeError for truly unsupported objects, otherwise you may get confusing errors.
Another edge case: orjson serializes float('nan') and float('inf') as null by default. This is different from json, which outputs NaN and Infinity (which are not valid JSON). If you need to preserve these values, you must use a custom serializer, but the standard JSON specification does not allow them, so the default behavior is often correct.
When to Use orjson in Production
orjson is a good fit for services that serialize JSON frequently, such as REST APIs, message queues, or caching layers. Its speed and native support for common types reduce boilerplate code. However, it is a third-party dependency, so you need to manage it in your deployment environment. The library is actively maintained and supports current Python versions, but you should verify compatibility with your Python runtime.
For applications that rely heavily on json's JSONEncoder subclassing or custom default behavior, migrating to orjson requires adjusting to its flag-based options. The API is similar enough that most code can be changed by replacing json.dumps with orjson.dumps and handling the bytes return type.
If you are working with NumPy arrays, orjson can serialize them efficiently with OPT_SERIALIZE_NUMPY, which is a significant advantage over json where you would need to convert arrays to lists manually. This makes orjson particularly useful in data pipelines and scientific computing contexts.
One final consideration: orjson is a compiled Rust extension. It may not be available on all platforms or may require a specific build process. For most mainstream Linux, macOS, and Windows environments, prebuilt wheels exist, but you should test your deployment target before relying on it.
Combining orjson with Pydantic and Other Libraries
orjson integrates well with libraries like Pydantic if you configure them to use it. Pydantic v2 supports orjson as a JSON encoder/decoder, which can speed up validation and serialization in FastAPI applications. The exact configuration depends on the library version, but the principle is to pass orjson.dumps as the json_dumps parameter.
from fastapi import FastAPI import orjson app = FastAPI() @app.get("/data") def get_data(): return {"message": "hello"}
FastAPI uses jsonable_encoder and the standard json module by default. To switch to orjson, you can set default_response_class or use a custom APIRoute class. This is an advanced topic, but it shows how orjson can be dropped into existing frameworks without rewriting your application logic.
When using orjson with libraries that expect str output, remember to decode the bytes result. For example, if you are writing to a file, you can use file.write(orjson.dumps(data)) directly because file objects accept bytes. If you are returning JSON from a web framework, most frameworks handle bytes fine, but check the documentation.
Handling Non-Serializable Types Gracefully
Even with orjson's built-in support, you will encounter objects that are not serializable. The default function is your escape hatch, but it must be designed carefully to avoid masking errors. A common pattern is to check for specific types and fall back to a generic representation, or raise TypeError to signal that the object cannot be serialized.
import orjson from decimal import Decimal def default(obj): if isinstance(obj, Decimal): return str(obj) raise TypeError(f"Object of type {type(obj).__name__} is not serializable") value = Decimal("3.14") print(orjson.dumps({"pi": value}, default=default)) # b'{"pi":"3.14"}'
Be aware that default is called for every unsupported object, including nested ones. If you have a large data structure with many unsupported objects, the overhead may be significant. In such cases, consider converting the data to a serializable form before calling dumps.
Also note that orjson does not support cyclic references. If your data contains a reference cycle, dumps() will raise a RecursionError or TypeError. The standard json module also fails on cycles, so this is not a regression, but it is worth remembering when serializing object graphs.
Final Implementation Notes
When you integrate orjson into your project, start by replacing the most performance-critical json calls. Measure the impact in your actual workload rather than assuming it will be faster. Keep the bytes vs str distinction in mind, and always handle TypeError and JSONDecodeError explicitly.
For serializing dataclasses, remember to pass OPT_SERIALIZE_DATACLASS. For datetime and UUID, use the appropriate flags. If you need to maintain compatibility with existing JSON consumers, test the output format carefully, especially the handling of timezone offsets and non-finite floats.
orjson is a robust tool for Python JSON serialization, but it is not a drop-in replacement for every use case. Its performance benefits and native type support make it a strong choice for modern Python services, provided you understand its options and limitations.