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}"
end

The 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
end

The *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
end

Use 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 automatically

Jbuilder'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 json gem after require 'json'. Returns a Hash with string keys for JSON objects, an Array for JSON arrays, String, Integer, Float, true, false, or nil for the corresponding JSON primitives. Accepts an options hash: symbolize_names: true converts all string keys to symbols (including nested), max_nesting: N limits recursion depth (default 100), and create_additions: false disables unsafe class hint deserialization. Raises JSON::ParserError on invalid JSON input.
symbolize_names
An option for JSON.parse that converts all Hash keys from Strings to Symbols during parsing. With symbolize_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 json gem 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 to to_json for string serialization. Override as_json in your classes to control which attributes, computed properties, and nested objects appear in JSON output. ActiveRecord's as_json accepts only:, except:, methods:, and include: options. When Rails calls render json: @model, it calls as_json first to obtain the Hash, then to_json to serialize it.
to_json
A method added to all Ruby objects by the json gem 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 calls as_json to obtain a Hash, then serializes that Hash. JSON.generate(object) and object.to_json produce identical output — to_json is syntactic sugar. Override to_json(*args) when you need to produce custom JSON string output that cannot be expressed as a simple Hash transformation; the *args parameter 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 json gem. The primary API is Oj.load(string) for parsing and Oj.dump(object) for serialization. Install via gem 'oj' in your Gemfile. For Rails, call Oj.optimize_rails in an initializer to replace the entire Rails JSON stack with Oj, affecting all render json: calls, request body parsing, and ActiveJob serialization. Supports multiple parsing modes: :strict for standard JSON only, :compat for stdlib json compatibility, and :rails for 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 a json builder object with methods that map to JSON keys: json.name @user.name, json.address { json.city @user.city } for nested objects, and json.posts @posts, :id, :title for arrays. Jbuilder templates integrate with Rails fragment caching via json.cache!, making them efficient for repeated renders of the same data. Unlike serializer classes, Jbuilder templates live in the app/views directory 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 moduleOfficial Ruby standard library documentation for JSON.parse, JSON.generate, and all options
  • Oj Gem on GitHubOj source, benchmarks, parsing modes, and Rails optimization documentation
  • Rails Guides: Rendering JSONOfficial Rails guide on render json:, status codes, and JSON response formatting
  • Jbuilder on GitHubJbuilder DSL reference, caching, and template examples for Rails JSON APIs
  • Blueprinter on GitHubBlueprinter serializer documentation with views, fields, associations, and extensions