Ruby JSON: JSON.parse, JSON.generate, as_json, Oj Gem & Rails
Last updated:
Ruby's standard library json gem parses JSON strings with JSON.parse(string) — returning a Hash with string keys — and serializes with JSON.generate(object) or object.to_json, both available after require 'json'. JSON.parse(string, symbolize_names: true) converts string keys to symbols, which is faster for lookups since Ruby symbol comparison is O(1) pointer equality rather than character-by-character string comparison. The Oj (Optimized JSON) gem is 2–10× faster than the standard library — replace stdlib JSON with require 'oj'; Oj.load(string) and Oj.dump(object) in performance-critical paths. This guide covers JSON.parse and JSON.generate with all options, the symbolize_names option, custom serialization with as_json and to_json, ActiveRecord model JSON rendering in Rails, the Oj gem for performance, and Jbuilder and Blueprinter for structured API responses.
JSON.parse and JSON.generate: The stdlib Basics
Ruby's json gem ships with the standard library — require 'json' is all you need, no Bundler installation required. JSON.parse deserializes a JSON string into Ruby types: JSON objects become Hashes with string keys, JSON arrays become Arrays, strings become Strings, numbers become Integer or Float, booleans become true/false, and null becomes nil. JSON.generate and to_json perform the inverse mapping.
require 'json'
# ── Parsing: JSON string → Ruby objects ──────────────────────────
json_str = '{"name":"Alice","age":30,"admin":true,"score":null,"tags":["ruby","json"]}'
data = JSON.parse(json_str)
# => {"name"=>"Alice", "age"=>30, "admin"=>true, "score"=>nil, "tags"=>["ruby","json"]}
data["name"] # => "Alice" (string key)
data["age"] # => 30 (Integer)
data["admin"] # => true (Boolean)
data["score"] # => nil (null → nil)
data["tags"] # => ["ruby", "json"]
# ── Serialization: Ruby objects → JSON string ─────────────────────
hash = { name: "Bob", age: 25, roles: ["editor"] }
JSON.generate(hash) # => '{"name":"Bob","age":25,"roles":["editor"]}'
hash.to_json # => '{"name":"Bob","age":25,"roles":["editor"]}' — identical
# Symbols as keys are converted to strings in JSON output
{ user_id: 1, active: true }.to_json
# => '{"user_id":1,"active":true}'
# ── Pretty printing ───────────────────────────────────────────────
puts JSON.pretty_generate(hash)
# {
# "name": "Bob",
# "age": 25,
# "roles": [
# "editor"
# ]
# }
# ── Type mapping reference ─────────────────────────────────────────
# Ruby Hash ↔ JSON object {"key": value}
# Ruby Array ↔ JSON array [1, 2, 3]
# Ruby String ↔ JSON string "text"
# Ruby Integer ↔ JSON number 42
# Ruby Float ↔ JSON number 3.14
# Ruby true/false ↔ JSON true/false
# Ruby nil ↔ JSON null
# ── Nested structures ──────────────────────────────────────────────
nested = JSON.parse('{"user":{"id":1,"address":{"city":"Tokyo"}}}')
nested["user"]["address"]["city"] # => "Tokyo"
# ── Parsing JSON arrays ────────────────────────────────────────────
users = JSON.parse('[{"id":1},{"id":2}]')
users.first["id"] # => 1
users.map { |u| u["id"] } # => [1, 2]A key pitfall: JSON.parse always returns string keys for Hash objects, even if you serialized with symbol keys. { user_id: 1 }.to_json produces {"user_id":1}, and parsing it back gives {"user_id"=>1} with a string key — not :user_id. This round-trip key-type mismatch is the most common source of nil lookup errors after parsing JSON. Either always use string keys in your code, or always parse with symbolize_names: true and document the convention at the top of the file.
Parsing Options: symbolize_names, max_nesting, and create_additions
JSON.parse accepts an options hash as its second argument. The three most important options are symbolize_names (convert string keys to symbols), max_nesting (limit recursion depth for security), and create_additions (control whether JSON class hints are honored — disable for untrusted input). Understanding these options is essential for writing safe, ergonomic Ruby JSON code.
require 'json'
json = '{"user":{"id":1,"name":"Alice","roles":["admin"]}}'
# ── symbolize_names: true ─────────────────────────────────────────
# Converts all Hash keys (including nested) from String to Symbol
data = JSON.parse(json, symbolize_names: true)
# => {user: {id: 1, name: "Alice", roles: ["admin"]}}
data[:user][:name] # => "Alice" (symbol key access)
data[:user][:roles] # => ["admin"]
# Why symbols? Symbol comparison is O(1) — Ruby interns symbols so
# :foo.equal?(:foo) is always true (same object_id).
# String comparison is O(n) — each character must be compared.
# In hot paths with many hash lookups, symbols are measurably faster.
# ── max_nesting: limit recursion depth ────────────────────────────
# Default is 100. Lower it to defend against deeply nested attacks.
safe_data = JSON.parse('{"a":{"b":{"c":1}}}', max_nesting: 10)
# => {"a"=>{"b"=>{"c"=>1}}} — within limit, no problem
begin
JSON.parse('{"a":{"b":{"c":{"d":{"e":1}}}}}', max_nesting: 3)
rescue JSON::NestingError => e
puts e.message # => "nesting of 4 is too deep"
end
# ── create_additions: false (security best practice) ──────────────
# The json gem supports "JSON additions" — class hints embedded in JSON
# that tell Ruby which class to instantiate during parsing.
# Example of a JSON addition: {"json_class":"Symbol","s":"foo"}
# This is a SECURITY RISK with untrusted input — disable it:
safe = JSON.parse(untrusted_input, create_additions: false)
# create_additions: false is the default in json gem >= 2.7.0
# For older versions, always pass it explicitly with untrusted data.
# ── Combining options ──────────────────────────────────────────────
config = JSON.parse(
File.read('config.json'),
symbolize_names: true,
max_nesting: 20,
create_additions: false
)
# ── JSON.load vs JSON.parse ────────────────────────────────────────
# JSON.load is an ALIAS for JSON.parse in Ruby's json gem.
# However, JSON.load historically enabled create_additions by default
# (allowing arbitrary object deserialization) — a security risk.
# ALWAYS use JSON.parse for untrusted input. JSON.load is for
# trusted internal data (e.g., config files, fixtures).
# Safe pattern for user-supplied JSON:
def safe_parse(input)
JSON.parse(input, max_nesting: 50, create_additions: false)
rescue JSON::ParserError => e
raise ArgumentError, "Invalid JSON: #{e.message}"
endThe create_additions option is the most important security control in Ruby JSON parsing. When enabled (historically the default for JSON.load), the json gem checks for a json_class key in the parsed object and instantiates that Ruby class — allowing an attacker to trigger arbitrary object construction during parsing. This was the source of multiple CVEs in Rails. As of json gem 2.7.0, create_additions defaults to false for JSON.parse, but always set it explicitly when parsing untrusted data for defense in depth. See the JSON security guide for more on safe parsing practices.
Custom Serialization: as_json and to_json
Ruby's json gem serializes objects by calling as_json to obtain a Hash representation, then to_json to convert that Hash to a JSON string. Override as_json in your classes to control which attributes, computed properties, and nested objects appear in JSON output. Define to_json when you need complete control over the raw string output. ActiveSupport (Rails) enriches as_json with additional options.
require 'json'
# ── Default serialization — only instance variables ───────────────
class Product
attr_accessor :id, :name, :price_cents, :created_at
def initialize(id, name, price_cents)
@id = id
@name = name
@price_cents = price_cents
@created_at = Time.now
end
end
p = Product.new(1, "Widget", 1999)
p.to_json
# => '{"id":1,"name":"Widget","price_cents":1999,"created_at":"2026-05-20 ..."}'
# All instance variables are serialized by default — may expose internals
# ── Override as_json to control output ────────────────────────────
class Product
def as_json(options = {})
{
id: @id,
name: @name,
price: @price_cents / 100.0, # transform: cents → dollars
created_at: @created_at.iso8601, # format DateTime as ISO 8601
}
end
def to_json(*args)
as_json.to_json(*args) # delegate to Hash#to_json
end
end
p.to_json
# => '{"id":1,"name":"Widget","price":19.99,"created_at":"2026-05-20T00:00:00Z"}'
# ── Nested objects: as_json is called recursively ─────────────────
class Order
def initialize(id, product, quantity)
@id = id
@product = product
@quantity = quantity
end
def as_json(options = {})
{
id: @id,
product: @product.as_json, # nested: calls Product#as_json
quantity: @quantity,
total: @product.instance_variable_get(:@price_cents) * @quantity / 100.0
}
end
def to_json(*args)
as_json.to_json(*args)
end
end
order = Order.new(42, p, 3)
puts JSON.pretty_generate(order.as_json)
# {
# "id": 42,
# "product": { "id": 1, "name": "Widget", "price": 19.99, ... },
# "quantity": 3,
# "total": 59.97
# }
# ── Conditional fields ─────────────────────────────────────────────
class User
def as_json(options = {})
base = { id: @id, name: @name, email: @email }
# Include sensitive fields only when explicitly requested
base[:admin] = @admin if options[:include_admin]
base
end
end
user.as_json # => {id:, name:, email:}
user.as_json(include_admin: true) # => {id:, name:, email:, admin:}
# ── Using to_json with options hash ───────────────────────────────
# The *args pattern passes options from Array/Hash to_json through to
# nested objects — essential for Rails' as_json(only:, except:, methods:)
class Article
def to_json(*args)
as_json.to_json(*args)
end
endThe *args parameter in to_json(*args) is important: it passes options through from the calling context (such as Rails' render json:) to the underlying Hash serialization. Without it, options like only: and except: passed to render json: @product, only: [:id, :name] would be silently ignored. For comprehensive JSON best practices including schema validation and versioning, see our dedicated guide.
Rails JSON Rendering: render json:, ActiveRecord, and as_json
Rails provides first-class JSON support through ActiveRecord's as_json method, controller render json:, and the respond_to format block. ActiveRecord's as_json accepts only:, except:, methods:, and include: options, making it easy to shape JSON output without custom serializers for simple cases.
# ── Basic render json: ────────────────────────────────────────────
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
render json: @user # calls @user.as_json.to_json, sets Content-Type: application/json
end
def create
@user = User.new(user_params)
if @user.save
render json: @user, status: :created # 201
else
render json: { errors: @user.errors }, status: :unprocessable_entity # 422
end
end
end
# ── ActiveRecord as_json options ──────────────────────────────────
user = User.find(1)
# Whitelist specific attributes
user.as_json(only: [:id, :name, :email])
# => {"id"=>1, "name"=>"Alice", "email"=>"alice@example.com"}
# Blacklist sensitive attributes
user.as_json(except: [:password_digest, :reset_token, :created_at])
# Include method results (computed properties)
user.as_json(methods: [:full_name, :avatar_url])
# => {"id"=>1, ..., "full_name"=>"Alice Smith", "avatar_url"=>"https://..."}
# Include associations
user.as_json(include: :posts)
# => {"id"=>1, ..., "posts"=>[{"id"=>10, "title"=>"Hello World",...}]}
# Nested include with options
user.as_json(
only: [:id, :name],
include: { posts: { only: [:id, :title, :published_at] } }
)
# ── Override as_json on the model ─────────────────────────────────
class User < ApplicationRecord
def as_json(options = {})
super(options.merge(
only: [:id, :name, :email, :created_at],
methods: [:avatar_url]
))
end
end
# Now render json: @user always uses the controlled output
render json: @user
# ── render json: with status and headers ──────────────────────────
render json: { message: "created" }, status: :created
render json: { error: "not found" }, status: :not_found # 404
render json: { error: "forbidden" }, status: :forbidden # 403
# ── respond_to: HTML and JSON from the same action ────────────────
def index
@users = User.all
respond_to do |format|
format.html # renders views/users/index.html.erb
format.json { render json: @users }
end
end
# ── Collection serialization ───────────────────────────────────────
render json: User.all.map { |u| u.as_json(only: [:id, :name]) }
# ── Rendering with a root key ─────────────────────────────────────
render json: { user: @user }
# => {"user": {"id":1, "name":"Alice", ...}}Avoid using render json: @user on models without overriding as_json — the default Rails implementation includes all attributes, which can accidentally expose sensitive fields like password_digest, remember_token, or internal timestamps. Always define a controlled as_json on each model, or use a serializer gem. For APIs serving multiple clients with different field requirements, serializer gems (Blueprinter, ActiveModelSerializers) provide versioned, reusable representations that are easier to maintain than per-model as_json overrides. See the JSON API design guide for structuring API responses.
The Oj Gem: 2–10× Faster JSON Parsing
Oj (Optimized JSON) is a C extension gem that replaces Ruby's pure-Ruby json stdlib with a native implementation. Benchmarks consistently show 2–10× improvements in both parsing and serialization speed. Oj is the standard JSON performance upgrade in high-throughput Rails and Sinatra APIs. Install with gem 'oj' in your Gemfile.
# Gemfile
gem 'oj', '~> 3.16'
# ── Basic usage ────────────────────────────────────────────────────
require 'oj'
json_str = '{"name":"Alice","age":30,"tags":["ruby","json"]}'
# Parse
data = Oj.load(json_str)
# => {"name"=>"Alice", "age"=>30, "tags"=>["ruby","json"]}
# Serialize
Oj.dump({ name: "Alice", age: 30 })
# => '{"name":"Alice","age":30}'
# Symbol keys when parsing
Oj.load(json_str, symbol_keys: true)
# => {name: "Alice", age: 30, tags: ["ruby", "json"]}
# Pretty print
Oj.dump({ name: "Alice" }, indent: 2)
# {
# "name": "Alice"
# }
# ── Oj parsing modes ───────────────────────────────────────────────
# :strict — standard JSON only (RFC 7159); raises on Ruby-specific extensions
# :compat — compatible with stdlib json gem behavior
# :rails — compatible with ActiveSupport JSON (Rails)
# :object — supports Oj object extensions (Ruby class hints)
# :wab — WabAB data format (for WAB databases)
Oj.load(json_str, mode: :strict) # safest for untrusted input
Oj.load(json_str, mode: :compat) # drop-in replacement for JSON.parse
# ── Replace Rails' default JSON backend with Oj ───────────────────
# config/initializers/oj.rb
require 'oj'
Oj.optimize_rails # replaces MultiJson, ActiveSupport::JSON with Oj
# This affects ALL JSON operations in Rails:
# - render json: @user
# - response.body parsing in tests
# - ActiveJob argument serialization
# - ActionDispatch request JSON parsing
# ── Benchmark: stdlib vs Oj ────────────────────────────────────────
require 'benchmark'
require 'json'
require 'oj'
large_json = File.read('large_payload.json') # ~1MB JSON file
Benchmark.bm(10) do |x|
x.report("JSON.parse") { 1000.times { JSON.parse(large_json) } }
x.report("Oj.load ") { 1000.times { Oj.load(large_json) } }
end
# Typical results:
# user system total real
# JSON.parse 8.450000 0.120000 8.570000 ( 8.612345)
# Oj.load 1.230000 0.010000 1.240000 ( 1.251234)
# => Oj.load is ~7x faster on large payloads
# ── Oj.mimic_JSON: make Oj respond to JSON.parse calls ────────────
Oj.mimic_JSON # after this, JSON.parse calls Oj internally
# Useful for codebases with many direct JSON.parse calls
# ── Streaming large JSON with Oj::Doc ─────────────────────────────
Oj::Doc.open(large_json) do |doc|
doc.each_leaf do |d|
puts d.where? # JSON path to current leaf
puts d.fetch # value at current position
end
endUse Oj.optimize_rails in an initializer as the single most impactful JSON performance change in a Rails app — it replaces the entire JSON stack with no code changes required. In mode :strict, Oj disables Ruby-specific JSON extensions and only parses standard JSON, which is the safest choice for untrusted input. For throughput-sensitive workloads like webhook ingestion or data pipeline processing, Oj is a non-negotiable optimization. See the JSON performance guide for additional strategies including caching and streaming.
Jbuilder and Blueprinter: API Response Templates
For Rails APIs that serve complex JSON responses with conditional fields, nested associations, and pagination, templating tools are better than as_json overrides in models. Jbuilder provides ERB-like view templates for JSON; Blueprinter provides a class-based serializer with a clean DSL. Both separate serialization logic from models, making it easy to maintain multiple representations of the same object.
# ── Jbuilder ──────────────────────────────────────────────────────
# Gemfile: gem 'jbuilder' (included in Rails by default)
# app/views/users/show.json.jbuilder
json.id @user.id
json.name @user.name
json.email @user.email
json.address do
json.street @user.address.street
json.city @user.address.city
json.zip @user.address.zip
end
json.posts @user.posts, :id, :title, :published_at
# Conditional fields
if current_user.admin?
json.role @user.role
json.last_login @user.last_sign_in_at
end
# Output:
# {
# "id": 1,
# "name": "Alice",
# "email": "alice@example.com",
# "address": {"street":"...", "city":"Tokyo", "zip":"100-0001"},
# "posts": [{"id":10, "title":"Hello World", "published_at":"..."}],
# "role": "admin",
# "last_login": "2026-05-19T..."
# }
# Controller — Jbuilder templates are rendered automatically
def show
@user = User.find(params[:id])
# Renders app/views/users/show.json.jbuilder when format is JSON
end
# ── Blueprinter ───────────────────────────────────────────────────
# Gemfile: gem 'blueprinter'
# app/blueprints/user_blueprint.rb
class UserBlueprint < Blueprinter::Base
identifier :id
fields :name, :email, :created_at
field :avatar_url do |user|
user.avatar.url
end
# Named views for different contexts
view :normal do
fields :name, :email
end
view :extended do
include_view :normal
fields :phone, :address
association :posts, blueprint: PostBlueprint
end
view :admin do
include_view :extended
fields :role, :last_sign_in_at, :sign_in_count
end
end
# Usage in controllers
render json: UserBlueprint.render(@user) # default view
render json: UserBlueprint.render(@user, view: :extended) # extended view
render json: UserBlueprint.render(@users, view: :normal) # collection
# With root key
render json: UserBlueprint.render(@user, root: :user)
# => {"user": {"id": 1, "name": "Alice", ...}}
# ── active_model_serializers ──────────────────────────────────────
# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email
attribute :full_name do
"#{object.first_name} #{object.last_name}"
end
has_many :posts, serializer: PostSerializer
belongs_to :organization
end
# Controller — automatically detected by Rails
render json: @user # picks up UserSerializer automaticallyJbuilder's template approach integrates naturally with Rails view caching — wrap Jbuilder partials with json.cache! to cache rendered JSON fragments using Rails' cache store. Blueprinter's named views are its strongest feature: define a :summary view for list endpoints and a :detail view for single-resource endpoints, keeping the same model from exposing too much or too little data depending on context. For teams debating serialization strategy, Blueprinter is typically easier to test (pure Ruby classes with no Rails dependency in tests) while Jbuilder is more flexible for deeply conditional JSON structures. More patterns in the JSON API design guide.
Security: JSON Injection, max_nesting, and Symbol Memory Leaks
Ruby JSON parsing has three primary security concerns: JSON injection (embedding raw JSON strings in templates without escaping), deeply nested JSON payloads that exhaust the stack (denial-of-service), and symbol interning attacks (pre-Ruby 2.2 memory exhaustion from parsing many unique keys with symbolize_names: true). Understanding and mitigating these is essential for production Rails APIs.
# ── JSON injection in ERB templates ──────────────────────────────
# UNSAFE: embedding user data directly in a <script> tag
# <script>var user = <%= @user.to_json %>;</script>
# If @user.name = '</script><script>alert("xss")</script>',
# the attacker breaks out of the JSON context into raw HTML.
# SAFE: use json_escape (Rails helper) to escape </script> sequences
# <script>var user = <%= json_escape(@user.to_json) %>;</script>
# SAFER: use the gon gem or data attributes instead of inline JSON
# <div data-user="<%= @user.to_json.html_escape %>"></div>
# Or: move data to a JSON API endpoint and fetch it with JavaScript.
# ── max_nesting: defend against deep-nesting DoS ─────────────────
# A malicious client can send: {"a":{"a":{"a":{"a": ...}}}}
# with hundreds of levels of nesting, causing deep recursion.
# Default nesting limit is 100. Reduce it for user-supplied input:
begin
safe = JSON.parse(request.body.read, max_nesting: 20, create_additions: false)
rescue JSON::NestingError
render json: { error: "JSON nesting too deep" }, status: :bad_request
return
rescue JSON::ParserError
render json: { error: "Invalid JSON" }, status: :bad_request
return
end
# Rails: rescue from parsing errors in ApplicationController
class ApplicationController < ActionController::API
rescue_from ActionDispatch::Http::Parameters::ParseError do |e|
render json: { error: "Invalid JSON in request body" }, status: :bad_request
end
end
# ── Symbol interning: memory risk with symbolize_names ────────────
# Symbols in Ruby < 2.2 are never garbage collected.
# An attacker sending JSON with many unique keys can exhaust memory:
# {"key1":"v","key2":"v","key3":"v",...,"key1000000":"v"}
# Each unique key string becomes a permanent symbol in memory.
# Ruby 2.2+ introduced symbol GC — dynamic symbols ARE collected.
# :foo created from "foo".to_sym is garbage-collectible in Ruby 2.2+.
# Literal symbols (:foo) are still permanent, but parse-created ones are not.
# Check your Ruby version: ruby --version
# Rule: always use symbolize_names: false (default) for untrusted JSON.
# Use symbolize_names: true only for trusted, schema-controlled input.
# ── create_additions: disable for untrusted input ─────────────────
# The json gem supports "JSON additions" — class hints that trigger
# Ruby class instantiation during parsing. This is a security risk.
# Example dangerous payload: {"json_class":"Kernel","s":"system('rm -rf /')"}
# (hypothetical — actual exploitability depends on json gem version)
# Always disable for untrusted input:
JSON.parse(user_input, create_additions: false) # explicit and safe
# ── Input size limits ─────────────────────────────────────────────
# Validate JSON payload size before parsing — large payloads consume memory.
MAX_JSON_SIZE = 1 * 1024 * 1024 # 1 MB
body = request.body.read(MAX_JSON_SIZE + 1)
if body.bytesize > MAX_JSON_SIZE
render json: { error: "Request body too large" }, status: :payload_too_large
return
end
data = JSON.parse(body, max_nesting: 20, create_additions: false)The json_escape Rails helper (also available as escape_javascript) is essential when embedding JSON in HTML <script> tags — it escapes </script>, <!--, and --> sequences that would allow an attacker to break out of the JSON context into raw HTML. For modern Rails apps, Content Security Policy headers provide a second layer of defense by blocking inline script execution. Apply payload size limits at the web server level (Nginx client_max_body_size) in addition to application-level checks. For a comprehensive treatment of these issues, see the JSON security guide.
Key Terms
- JSON.parse
- The primary method for deserializing a JSON string into Ruby objects, provided by Ruby's stdlib
jsongem afterrequire 'json'. Returns a Hash with string keys for JSON objects, an Array for JSON arrays, String, Integer, Float,true,false, ornilfor the corresponding JSON primitives. Accepts an options hash:symbolize_names: trueconverts all string keys to symbols (including nested),max_nesting: Nlimits recursion depth (default 100), andcreate_additions: falsedisables unsafe class hint deserialization. RaisesJSON::ParserErroron invalid JSON input. - symbolize_names
- An option for
JSON.parsethat converts all Hash keys from Strings to Symbols during parsing. Withsymbolize_names: true, the JSON object{"name":"Alice"}parses to{ name: "Alice" }instead of{ "name" => "Alice" }. Symbol lookup is O(1) because Ruby interns symbols — the same symbol literal is always the same object in memory, so equality comparison is a pointer check rather than a string traversal. Apply to all nested keys automatically, including those inside nested JSON objects. Do not use with untrusted input containing many unique keys in Ruby versions before 2.2, where symbols were never garbage-collected. - as_json
- A Ruby method defined by the
jsongem and extended by ActiveSupport (Rails) on all objects. It returns a JSON-compatible Ruby representation of the object — typically a Hash — which is then passed toto_jsonfor string serialization. Overrideas_jsonin your classes to control which attributes, computed properties, and nested objects appear in JSON output. ActiveRecord'sas_jsonacceptsonly:,except:,methods:, andinclude:options. When Rails callsrender json: @model, it callsas_jsonfirst to obtain the Hash, thento_jsonto serialize it. - to_json
- A method added to all Ruby objects by the
jsongem that serializes the object to a JSON string. For basic types (Hash, Array, String, Integer, Float,true,false,nil), it produces the standard JSON encoding. For custom objects, the default implementation callsas_jsonto obtain a Hash, then serializes that Hash.JSON.generate(object)andobject.to_jsonproduce identical output —to_jsonis syntactic sugar. Overrideto_json(*args)when you need to produce custom JSON string output that cannot be expressed as a simple Hash transformation; the*argsparameter passes options through to nested serializations. - Oj gem
- Optimized JSON — a Ruby gem providing a C extension for JSON parsing and serialization that is 2–10× faster than the standard library
jsongem. The primary API isOj.load(string)for parsing andOj.dump(object)for serialization. Install viagem 'oj'in your Gemfile. For Rails, callOj.optimize_railsin an initializer to replace the entire Rails JSON stack with Oj, affecting allrender json:calls, request body parsing, and ActiveJob serialization. Supports multiple parsing modes::strictfor standard JSON only,:compatfor stdlib json compatibility, and:railsfor ActiveSupport compatibility. - Jbuilder
- A Rails gem (included by default) that provides a DSL for building JSON responses using view template files (
.json.jbuilder). Templates use ajsonbuilder object with methods that map to JSON keys:json.name @user.name,json.address { json.city @user.city }for nested objects, andjson.posts @posts, :id, :titlefor arrays. Jbuilder templates integrate with Rails fragment caching viajson.cache!, making them efficient for repeated renders of the same data. Unlike serializer classes, Jbuilder templates live in theapp/viewsdirectory alongside HTML templates and are rendered via the normal Rails view rendering pipeline.
FAQ
How do I parse JSON in Ruby?
Require the json gem and call JSON.parse(string): require 'json'; data = JSON.parse('{"name":"Alice"}'). This returns a Ruby Hash with string keys — data["name"] returns "Alice". The json gem is part of Ruby's standard library, so no Gemfile entry is needed unless you want a specific version. For faster parsing in high-throughput applications, use the Oj gem: require 'oj'; Oj.load(string). Wrap the call in a rescue JSON::ParserError block to handle malformed input gracefully. If your application already uses Rails, JSON.parse is available without an explicit require since ActiveSupport loads the json gem automatically.
How do I get symbol keys when parsing JSON in Ruby?
Pass symbolize_names: true to JSON.parse: data = JSON.parse(json_string, symbolize_names: true). This converts all Hash keys from strings to symbols, including nested keys. Access values with data[:name] instead of data["name"]. Symbol lookups are O(1) because Ruby interns symbols — every :name reference points to the same object in memory, making equality comparison a pointer check. Do not use symbolize_names: true with untrusted input containing many unique keys if running Ruby older than 2.2, where symbols were not garbage-collected and unique symbol creation could exhaust memory. In Ruby 2.2+, dynamic symbols created at runtime are garbage-collectable, making this safe.
How do I serialize a Ruby object to JSON?
After require 'json', call object.to_json or JSON.generate(object) — both produce identical output. For Hashes: { name: "Alice", age: 30 }.to_json produces '{"name":"Alice","age":30}' — symbol keys are automatically converted to strings in JSON. For Arrays: [1, 2, 3].to_json produces '[1,2,3]'. For custom classes, define as_json(options = ) returning a Hash, then to_json(*args); as_json.to_json(*args); end. For pretty output use JSON.pretty_generate(object). With the Oj gem: Oj.dump(object) is 2–10× faster than to_json.
How do I customize JSON output for a Rails model?
Override as_json on the ActiveRecord model to control the JSON representation: def as_json(options = ); super(only: [:id, :name, :email], methods: [:full_name]); end. The super call with only: whitelists attributes; except: blacklists them; methods: adds method results as fields; include: adds associations. After overriding as_json, render json: @model in any controller automatically uses the controlled output. For multiple representations (summary vs. detail), use serializer gems: Blueprinter with named views (view :summary, view :detail) or Jbuilder templates in app/views. Avoid ad-hoc render json: @model.as_json(only: [...]) scattered across controllers — it is hard to maintain consistently.
What is the Oj gem and when should I use it?
Oj (Optimized JSON) is a C extension gem that replaces Ruby's stdlib JSON with a native implementation that is 2–10× faster. Add gem 'oj' to your Gemfile and use Oj.load(string) to parse and Oj.dump(object) to serialize. For Rails, add require 'oj'; Oj.optimize_rails in an initializer (config/initializers/oj.rb) to replace the entire JSON stack with no other code changes. Use Oj when JSON processing appears in performance profiling hot paths — API ingestion endpoints, webhook handlers, or data transformation pipelines processing thousands of records per second. In most Rails apps serving under 1,000 requests per minute, the difference is negligible; in high-throughput services, Oj is a standard optimization.
How do I handle JSON parsing errors in Ruby?
Wrap JSON.parse in a rescue JSON::ParserError block: begin; data = JSON.parse(input); rescue JSON::ParserError => e; puts "Invalid JSON: #{e.message}"; end. JSON::ParserError is a subclass of StandardError. Common causes of invalid JSON from HTTP APIs: trailing commas (valid JS, invalid JSON), single-quoted strings (invalid JSON), unquoted keys, and truncated responses. For the Oj gem, rescue Oj::ParseError instead. In Rails, ActionDispatch::Http::Parameters::ParseError is raised when a request body contains malformed JSON — rescue it in ApplicationController with rescue_from ActionDispatch::Http::Parameters::ParseError { render json: { error: "Invalid JSON" }, status: :bad_request }.
How do I render JSON in a Rails controller?
Use render json: object in any Rails controller action. Rails calls object.as_json to get a Hash, then to_json to serialize it, and sets Content-Type: application/json. Add a status code with render json: { error: "not found" }, status: :not_found (uses named HTTP status symbols) or status: 404 (numeric). For structured API responses, use serializer gems: render json: UserBlueprint.render(@user) (Blueprinter) or define a Jbuilder template at app/views/users/show.json.jbuilder that Rails renders automatically when the request format is JSON. Use respond_to { |format| format.json { render json: @user } } in controllers that serve both HTML and JSON.
How do I pretty print JSON in Ruby?
Use JSON.pretty_generate(object) instead of JSON.generate(object) or object.to_json. It formats output with 2-space indentation and newlines, making it human-readable. Customize indentation: JSON.pretty_generate(data, indent: " ") for 4 spaces. To write pretty JSON to a file: File.write("output.json", JSON.pretty_generate(data)). For the Oj gem: Oj.dump(object, indent: 2) adds 2-space indentation. In a Rails console, pp JSON.parse(json_string) pretty-prints the Ruby Hash using Ruby's built-in pretty-printer — useful for debugging API responses. Never use pretty_generate in production API responses — the extra whitespace increases payload size unnecessarily.
Further reading and primary sources
- Ruby Docs: JSON module — Official Ruby standard library documentation for JSON.parse, JSON.generate, and all options
- Oj Gem on GitHub — Oj source, benchmarks, parsing modes, and Rails optimization documentation
- Rails Guides: Rendering JSON — Official Rails guide on render json:, status codes, and JSON response formatting
- Jbuilder on GitHub — Jbuilder DSL reference, caching, and template examples for Rails JSON APIs
- Blueprinter on GitHub — Blueprinter serializer documentation with views, fields, associations, and extensions