Oj is a high-performance JSON parser and generator for Ruby that delivers exceptional speed while maintaining ease of use. Its remarkable efficiency has made it the go-to choice for Ruby developers who need to handle JSON data at scale, often outperforming the standard library's JSON implementation by several orders of magnitude.
Oj supports all the essential capabilities you'd expect from a production-grade JSON library, including customizable parsing modes, symbol key handling, and extensive serialization options. What sets it apart is its flexibility—you can configure Oj to work exactly how you need it to, whether you're building a high-throughput API, processing large datasets, or serializing complex Ruby objects for caching.
This guide will walk you through integrating Oj into your Ruby applications and leveraging its powerful features to optimize JSON handling. You'll discover how to configure the library for different scenarios and understand the tradeoffs between its various operating modes.
Prerequisites
Before diving into the rest of this article, make sure you have a recent version of Ruby installed on your machine. This tutorial also assumes basic familiarity with Ruby syntax and working with gems through Bundler.
Getting started with Oj
To follow along with this tutorial effectively, create a new Ruby project where you can experiment with the concepts we'll cover. Begin by setting up a new project directory:
mkdir oj-json-parsing && cd oj-json-parsing
bundle init
Next, add Oj to your Gemfile:
source 'https://rubygems.org'
gem 'oj'
Install the gem using Bundler:
bundle install
Create a new json_parser.rb file in your project directory and add the following code:
require 'oj'
json_string = '{"name":"Alice","age":30,"active":true}'
parsed = Oj.load(json_string)
puts parsed.inspect
This snippet requires the oj gem and uses the Oj.load method to parse a JSON string into a Ruby hash.
Now create a simple script to test the parser:
require_relative 'json_parser'
Run the program with the following command:
ruby main.rb
You should see output similar to this:
{"name" => "Alice", "age" => 30, "active" => true}
The first thing worth noting is that Oj converts the JSON object into a Ruby hash with string keys by default. The parser automatically handles type conversions—strings remain strings, numbers become integers or floats, and boolean values translate to Ruby's true and false.
We'll explore how to customize key formats and adjust other parsing behaviors throughout this guide.
Understanding Oj's parsing modes
Oj provides several modes that control how it parses and generates JSON, each optimized for different use cases. Understanding these modes is crucial for getting the most out of the library.
Strict mode
Strict mode adheres closely to the JSON specification, making it ideal when you need guaranteed compliance with the standard:
require 'oj'
Oj.default_options = { mode: :strict }
json = Oj.dump({ name: 'Bob', age: 30 })
puts json
Run the file:
ruby strict_mode.rb
{"name":"Bob","age":30}
Strict mode only allows basic JSON-compatible types: strings, numbers, booleans, arrays, hashes, and nil. If you try to serialize objects that lack a clear JSON representation, like Time objects, strict mode will throw an error rather than attempting to convert them:
require 'oj'
Oj.default_options = { mode: :strict }
json = Oj.dump({ name: 'Bob', created_at: Time.now })
puts json
strict_mode.rb:5:in 'Oj.dump': Failed to dump Time Object to JSON in strict mode. (TypeError)
This behavior protects you from accidentally producing non-standard JSON that might break when consumed by other systems. When you need to include time values in strict mode, convert them to strings explicitly first.
Compat mode
Compat mode aims for compatibility with Ruby's standard JSON library while offering better performance:
require 'oj'
Oj.default_options = { mode: :compat }
data = {
user: 'Charlie',
timestamp: Time.now,
ratio: 3.14159
}
json = Oj.dump(data)
puts json
Run the program with the following command:
ruby compat_mode.rb
{"user":"Charlie","timestamp":"2025-11-04 07:31:16 +0200","ratio":3.14159}
This mode handles a wider range of Ruby objects than strict mode, making it a drop-in replacement for the standard library in most cases. It's the recommended mode when you're migrating from JSON to Oj.
Object mode
Object mode is designed for serializing Ruby objects with their full type information, enabling round-trip serialization:
require 'oj'
class User
attr_accessor :name, :email
def initialize(name, email)
@name = name
@email = email
end
end
Oj.default_options = { mode: :object }
user = User.new('Diana', 'diana@example.com')
json = Oj.dump(user)
puts json
restored = Oj.load(json)
puts "Restored: #{restored.name} - #{restored.email}"
Execute the code with the following command:
ruby object_mode.rb
{"^o":"User","name":"Diana","email":"diana@example.com"}
Restored: Diana - diana@example.com
The ^o field stores type information, allowing Oj to reconstruct the original Ruby object during parsing. This mode is particularly useful for caching complex objects or implementing custom serialization systems.
Null mode
Null mode prioritizes raw speed over everything else, skipping most validation and type checking:
require 'oj'
Oj.default_options = { mode: :null }
large_data = { items: (1..10000).map { |i| { id: i, value: "item_#{i}" } } }
json = Oj.dump(large_data)
puts "Generated #{json.length} bytes"
Run the script to see the output:
ruby null_mode.rb
Generated 317799 bytes
This mode is ideal for scenarios where you control both ends of the serialization process and need maximum throughput. Use it when processing large volumes of trusted data.
Configuring key types in Oj
By default, Oj converts JSON object keys to strings, but you can modify this behavior to match your application's conventions.
Using symbol keys
Many Ruby developers prefer symbols over strings for hash keys. You can configure Oj to automatically convert string keys to symbols:
require 'oj'
json = '{"first_name":"Emma","last_name":"Wilson","age":28}'
hash = Oj.load(json, symbol_keys: true)
puts hash.inspect
puts "Name: #{hash[:first_name]} #{hash[:last_name]}"
Execute the program using this command:
ruby symbol_keys.rb
{first_name: "Emma", last_name: "Wilson", age: 28}
Name: Emma Wilson
Symbol keys can make your code cleaner and slightly more performant, but remember that symbols aren't garbage collected in older Ruby versions, so use this option carefully with untrusted input.
Setting default options
Rather than passing options on every call, you can set defaults that apply globally:
require 'oj'
Oj.default_options = {
mode: :compat,
symbol_keys: true,
indent: 2
}
data = { status: 'success', count: 42 }
json = Oj.dump(data)
puts json
parsed = Oj.load(json)
puts parsed.class
puts parsed.keys.first.class
Run the code with the following command:
ruby default_options.rb
{
"status":"success",
"count":42
}
Hash
Symbol
This approach keeps your code cleaner and ensures consistent behavior throughout your application. You can still override these defaults on individual calls when needed.
Generating formatted JSON with Oj
While compact JSON is perfect for APIs and data transmission, human-readable formatting becomes invaluable during development and debugging.
Basic pretty printing
Add indentation and line breaks to make JSON easier to read:
require 'oj'
user = {
id: 1001,
profile: {
name: 'Frank Miller',
bio: 'Software developer and writer',
location: 'Seattle'
},
interests: ['coding', 'photography', 'hiking']
}
pretty_json = Oj.dump(user, indent: 2)
puts pretty_json
Run the script to see the formatted output:
ruby pretty_print.rb
{
":id":1001,
":profile":{
":name":"Frank Miller",
":bio":"Software developer and writer",
":location":"Seattle"
},
":interests":[
"coding",
"photography",
"hiking"
]
}
The indent option controls how many spaces to use for each level of nesting. This format makes it much easier to spot issues in your JSON structure during development.
Customizing output format
You can control various aspects of the generated JSON to match specific requirements:
require 'oj'
data = {
timestamp: Time.now,
temperature: 72.5,
conditions: ['sunny', 'warm', 'clear']
}
json = Oj.dump(data, {
indent: 4,
space: ' ',
space_before: ' ',
object_nl: "\n",
array_nl: "\n"
})
puts json
These options give you fine-grained control over whitespace, making it possible to match specific formatting standards or preferences required by external systems.
Working with files in Oj
Reading from and writing to files is a common operation when dealing with JSON data. Oj provides convenient methods for both.
Writing JSON to files
Save Ruby objects as JSON files directly:
require 'oj'
users = [
{ id: 1, username: 'alice', email: 'alice@example.com' },
{ id: 2, username: 'bob', email: 'bob@example.com' },
{ id: 3, username: 'charlie', email: 'charlie@example.com' }
]
Oj.to_file('users.json', users, indent: 2)
puts 'Data written to users.json'
Execute the script with this command:
ruby write_file.rb
Data written to users.json
This creates a properly formatted JSON file that you can inspect or share with other systems. The indent option ensures the output is readable.
Reading JSON from files
Load JSON data from files just as easily:
require 'oj'
loaded_users = Oj.load_file('users.json', symbol_keys: true)
loaded_users.each do |user|
puts "User #{user[:id]}: #{user[:username]} (#{user[:email]})"
end
Run the program to see the loaded data:
ruby read_file.rb
User 1: alice (alice@example.com)
User 2: bob (bob@example.com)
User 3: charlie (charlie@example.com)
The load_file method handles opening the file, parsing its contents, and returning the Ruby data structure in one operation. This approach is more efficient than manually reading the file and parsing its contents separately.
Debugging JSON parsing issues
When working with JSON from external sources, you'll occasionally encounter parsing errors that need investigation.
Handling malformed JSON
Oj provides detailed error messages when parsing fails:
require 'oj'
bad_json_samples = [
'{"name": "Alice", "age": }',
'{"items": [1, 2, 3,]}',
"{'single': 'quotes'}",
'{"trailing": "comma",}'
]
bad_json_samples.each_with_index do |json, index|
begin
result = Oj.load(json)
puts "Sample #{index + 1}: Parsed successfully"
rescue Oj::ParseError => e
puts "Sample #{index + 1}: #{e.message}"
puts " Problem: #{json}"
puts
end
end
Execute the script to see the error handling in action:
ruby error_handling.rb
Sample 1: expected hash value, not a hash close (after age) at line 1, column 26 [parse.c:653]
Problem: {"name": "Alice", "age": }
Sample 2: expected array element, not an array close (after items) at line 1, column 20 [parse.c:628]
Problem: {"items": [1, 2, 3,]}
Sample 3: unexpected character (after ) at line 1, column 2 [parse.c:774]
Problem: {'single': 'quotes'}
Sample 4: expected hash key, not a hash close (after trailing) at line 1, column 22 [parse.c:653]
Problem: {"trailing": "comma",}
The error messages include the line and column number where the problem occurred, making it easier to locate issues in large JSON files.
Validating JSON structure
Sometimes you want to verify JSON is valid without fully parsing it:
require 'oj'
def valid_json?(string)
Oj.load(string)
true
rescue Oj::ParseError
false
end
test_cases = [
['{"valid": true}', true],
['{"invalid": }', false],
['[]', true],
['{"nested": {"deeply": {"valid": true}}}', true]
]
test_cases.each do |json, expected|
result = valid_json?(json)
status = result == expected ? "✓" : "✗"
puts "#{status} #{json[0..30]}... => #{result}"
end
Run the validation script:
ruby validation.rb
✓ {"valid": true}... => true
✓ {"invalid": }... => false
✓ []... => true
✓ {"nested": {"deeply": {"valid":... => true
This pattern is useful when accepting JSON from user input or external APIs where you need to validate before processing.
Final thoughts
Throughout this guide, we've covered the essentials of using Oj for JSON processing in Ruby, from basic parsing and generation to advanced features like custom serialization and performance optimization. You should now feel comfortable integrating Oj into your Ruby projects and leveraging its speed advantages.
Oj offers many additional capabilities beyond what we've covered here, so be sure to explore the official documentation to discover more advanced features like stream parsing, custom handlers, and specialized encoding options.
Thanks for reading, and happy coding!