JSON #
JSON (JavaScript Object Notation) adalah format pertukaran data paling populer di dunia web modern — hampir setiap API yang kamu konsumsi atau bangun menggunakan JSON. Ruby menyertakan library json di standard library sejak versi 1.9, jadi tidak perlu instalasi tambahan untuk kebutuhan dasar. Tapi ada banyak nuansa yang tidak langsung obvious: bagaimana cara mengonversi key String ke Symbol secara aman, bagaimana mengatur serialisasi objek kustom, bagaimana menangani JSON yang tidak valid tanpa crash, dan kapan perlu beralih ke parser yang lebih cepat seperti oj. Artikel ini membahas semua ini dari fondasi hingga pola yang digunakan di API produksi.
Memuat Library JSON #
# Standard library — sudah bawaan Ruby, tidak perlu install
require 'json'
# Atau gunakan gem oj untuk performa lebih tinggi (perlu install)
# gem install oj
require 'oj'
Parsing JSON — String ke Ruby Object #
JSON.parse mengonversi string JSON menjadi objek Ruby. Secara default, key JSON (yang selalu string) dikonversi menjadi String Ruby:
require 'json'
# JSON sederhana
json_str = '{"nama":"Rina","umur":28,"aktif":true}'
data = JSON.parse(json_str)
puts data.class # => Hash
puts data["nama"] # => "Rina"
puts data["umur"] # => 28
puts data["aktif"] # => true
# JSON array
json_arr = '[1, 2, 3, "empat", true, null]'
arr = JSON.parse(json_arr)
puts arr.inspect # => [1, 2, 3, "empat", true, nil]
# null JSON → nil Ruby
# JSON bersarang
json_kompleks = '{
"pengguna": {
"id": 1,
"nama": "Budi",
"alamat": {
"kota": "Jakarta",
"kode_pos": "10110"
},
"hobi": ["membaca", "coding", "hiking"]
}
}'
data = JSON.parse(json_kompleks)
puts data["pengguna"]["nama"] # => "Budi"
puts data["pengguna"]["alamat"]["kota"] # => "Jakarta"
puts data["pengguna"]["hobi"].first # => "membaca"
symbolize_names — Key sebagai Symbol #
Defaultnya key adalah String. Untuk mendapatkan key sebagai Symbol (lebih umum di Ruby idiomatik):
json_str = '{"nama":"Rina","umur":28,"kota":"Bandung"}'
# Dengan symbolize_names: true
data = JSON.parse(json_str, symbolize_names: true)
puts data[:nama] # => "Rina" (Symbol, bukan String)
puts data[:umur] # => 28
# Berlaku secara rekursif — semua level bersarang
json_bersarang = '{"user":{"nama":"Budi","alamat":{"kota":"Jakarta"}}}'
data = JSON.parse(json_bersarang, symbolize_names: true)
puts data[:user][:alamat][:kota] # => "Jakarta"
# ANTI-PATTERN: mencampur String dan Symbol key
data = JSON.parse(json_str) # key sebagai String
puts data[:nama] # => nil! (mencari Symbol :nama, tapi keynya String "nama")
puts data["nama"] # => "Rina"
# BENAR: konsisten — pilih satu dan pakai selamanya
# Untuk API response yang sering diakses, symbolize_names lebih nyaman
data = JSON.parse(json_str, symbolize_names: true)
puts data[:nama] # => "Rina"
Mapping Tipe Data JSON ↔ Ruby #
JSON Ruby
───────────────────────────────
object {} → Hash
array [] → Array
string "" → String
number int → Integer
number float→ Float
true → true (TrueClass)
false → false (FalseClass)
null → nil (NilClass)
Penanganan Error Parsing #
JSON yang tidak valid akan melempar exception. Selalu tangani ini ketika input berasal dari sumber eksternal:
# JSON tidak valid melempar JSON::ParserError
begin
data = JSON.parse("{ini bukan json}")
rescue JSON::ParserError => e
puts "JSON tidak valid: #{e.message}"
end
# Pola yang lebih ringkas dengan rescue inline
data = JSON.parse(teks) rescue nil
puts data.nil? ? "Gagal parse" : "Berhasil"
# Versi yang lebih informatif — buat helper
def parse_json_aman(teks, default: nil)
JSON.parse(teks, symbolize_names: true)
rescue JSON::ParserError => e
Rails.logger.warn("JSON::ParserError: #{e.message}") if defined?(Rails)
default
end
# Penggunaan
data = parse_json_aman(response.body, default: {})
data = parse_json_aman("[1,2,3]") # => [1, 2, 3]
data = parse_json_aman("bukan json") # => {} (default)
data = parse_json_aman(nil, default: []) # => [] (nil input)
Membuat JSON — Ruby Object ke String #
JSON.generate mengonversi objek Ruby menjadi string JSON:
require 'json'
# Hash sederhana
hash = { nama: "Citra", umur: 25, kota: "Surabaya" }
puts JSON.generate(hash)
# => {"nama":"Citra","umur":25,"kota":"Surabaya"}
# Array
arr = [1, "dua", :tiga, 4.0, true, nil, [5, 6]]
puts JSON.generate(arr)
# => [1,"dua","tiga",4.0,true,null,[5,6]]
# Struktur bersarang
data = {
id: 1,
pengguna: { nama: "Andi", email: "[email protected]" },
produk: [{ id: 10, nama: "Laptop" }, { id: 11, nama: "Mouse" }],
metadata: { created_at: Time.now.iso8601, version: "1.0" }
}
puts JSON.generate(data)
pretty_generate — JSON yang Mudah Dibaca #
data = {
status: "sukses",
data: {
pengguna: { id: 1, nama: "Rina" },
pesanan: [{ id: 100, total: 150_000 }]
}
}
# Kompak — untuk API response (ukuran lebih kecil)
puts JSON.generate(data)
# Pretty — untuk file konfigurasi atau debugging
puts JSON.pretty_generate(data)
# => {
# "status": "sukses",
# "data": {
# "pengguna": {
# "id": 1,
# "nama": "Rina"
# },
# "pesanan": [
# {
# "id": 100,
# "total": 150000
# }
# ]
# }
# }
# Simpan ke file konfigurasi yang bisa dibaca manusia
File.write("config.json", JSON.pretty_generate(data))
Shortcut — .to_json dan .to_json pada Objek #
Ruby menambahkan method to_json ke semua tipe data dasar:
{ nama: "Rina" }.to_json # => '{"nama":"Rina"}'
[1, 2, 3].to_json # => '[1,2,3]'
"halo".to_json # => '"halo"'
42.to_json # => '42'
true.to_json # => 'true'
nil.to_json # => 'null'
Time.now.to_json # => '"2024-08-15 14:30:00 +0700"'
Serialisasi Objek Kustom #
Secara default, objek kustom tidak bisa langsung di-serialisasi ke JSON. Ada beberapa cara untuk mengatasinya:
Cara 1: Implementasi to_json
#
class Produk
attr_reader :id, :nama, :harga, :aktif
def initialize(id, nama, harga, aktif: true)
@id = id
@nama = nama
@harga = harga
@aktif = aktif
end
def to_json(*args)
{
id: @id,
nama: @nama,
harga: @harga,
aktif: @aktif
}.to_json(*args)
end
end
produk = Produk.new(1, "Laptop", 15_000_000)
puts produk.to_json
# => {"id":1,"nama":"Laptop","harga":15000000,"aktif":true}
# Bekerja juga dalam koleksi
produk_list = [
Produk.new(1, "Laptop", 15_000_000),
Produk.new(2, "Mouse", 350_000, aktif: false)
]
puts produk_list.to_json
Cara 2: as_json — Cara yang Direkomendasikan Rails
#
Rails/ActiveSupport menyediakan as_json yang menghasilkan Hash Ruby (bukan String), yang kemudian di-serialize oleh to_json. Ini lebih fleksibel karena bisa dikustomisasi dengan opsi:
class Pengguna
attr_reader :id, :nama, :email, :password_hash, :created_at
def initialize(id, nama, email, password_hash)
@id = id
@nama = nama
@email = email
@password_hash = password_hash
@created_at = Time.now
end
def as_json(opsi = {})
hasil = {
id: @id,
nama: @nama,
email: @email,
created_at: @created_at.iso8601
# password_hash TIDAK disertakan — data sensitif!
}
# Dukung opsi :only dan :except seperti ActiveRecord
if opsi[:only]
hasil.slice(*Array(opsi[:only]))
elsif opsi[:except]
hasil.except(*Array(opsi[:except]))
else
hasil
end
end
def to_json(opsi = {})
as_json(opsi).to_json
end
end
user = Pengguna.new(1, "Rina", "[email protected]", "hash123secret")
puts user.to_json
# => {"id":1,"nama":"Rina","email":"[email protected]","created_at":"..."}
# password_hash tidak muncul!
puts user.to_json(only: [:id, :nama])
# => {"id":1,"nama":"Rina"}
Cara 3: Representasi Terpisah (Presenter/Serializer Pattern) #
Untuk aplikasi besar, pisahkan logika serialisasi ke kelas tersendiri:
class ProdukSerializer
def initialize(produk)
@produk = produk
end
def as_json
{
id: @produk.id,
nama: @produk.nama,
harga: format_harga(@produk.harga),
harga_angka: @produk.harga,
tersedia: @produk.stok > 0,
url: "/produk/#{@produk.id}"
}
end
def to_json
as_json.to_json
end
private
def format_harga(angka)
"Rp #{angka.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1.').reverse}"
end
end
# Penggunaan
produk = Produk.find(1)
puts ProdukSerializer.new(produk).to_json
# Untuk koleksi
produk_list = Produk.all.map { |p| ProdukSerializer.new(p).as_json }
puts JSON.generate(produk_list)
Opsi Parsing dan Generate Lanjutan #
# Opsi parsing
JSON.parse(json_str,
symbolize_names: true, # key sebagai Symbol
allow_nan: true, # izinkan NaN dan Infinity
max_nesting: 50 # kedalaman bersarang maksimal (default: 19)
)
# Opsi generate
JSON.generate(data,
allow_nan: true, # izinkan NaN dan Infinity di output
max_nesting: 50 # kedalaman bersarang maksimal
)
# pretty_generate dengan opsi indentasi kustom
puts JSON.pretty_generate(data) # default 2 spasi
# Indentasi kustom — JSON.state
state = JSON::State.new(
indent: " ", # 4 spasi
space: " ", # spasi setelah titik dua
space_before: "", # spasi sebelum titik dua
object_nl: "\n", # newline setelah setiap pasangan key-value
array_nl: "\n" # newline setelah setiap elemen array
)
puts JSON.generate(data, state)
Membaca dan Menulis File JSON #
# Membaca file JSON
def baca_json(path)
konten = File.read(path)
JSON.parse(konten, symbolize_names: true)
rescue Errno::ENOENT
raise "File tidak ditemukan: #{path}"
rescue JSON::ParserError => e
raise "File JSON tidak valid: #{e.message}"
end
config = baca_json("config/database.json")
puts config[:host] # => "localhost"
# Menulis file JSON
def tulis_json(path, data, pretty: false)
json_str = pretty ? JSON.pretty_generate(data) : JSON.generate(data)
File.write(path, json_str)
rescue IOError => e
raise "Gagal menulis file: #{e.message}"
end
tulis_json("output/hasil.json", { sukses: true, data: [1, 2, 3] })
tulis_json("config/settings.json", settings, pretty: true)
# Memperbarui file JSON yang sudah ada
def perbarui_json(path, &blok)
data = baca_json(path)
data_baru = blok.call(data)
tulis_json(path, data_baru, pretty: true)
end
perbarui_json("config/settings.json") do |config|
config.merge(versi: "2.0", diperbarui: Time.now.iso8601)
end
JSON Lines — Satu Objek per Baris #
JSON Lines (JSONL) adalah format di mana setiap baris adalah dokumen JSON yang valid secara terpisah. Sangat berguna untuk streaming data besar atau log:
# Menulis JSON Lines
File.open("events.jsonl", "a") do |f|
{ event: "login", user_id: 1, waktu: Time.now.iso8601 }.tap { |e| f.puts JSON.generate(e) }
{ event: "view", user_id: 1, halaman: "/produk", waktu: Time.now.iso8601 }.tap { |e| f.puts JSON.generate(e) }
end
# Membaca JSON Lines — efisien untuk file besar
def baca_jsonl(path)
File.foreach(path).map { |baris| JSON.parse(baris.chomp, symbolize_names: true) }
end
# Atau dengan lazy evaluation untuk file sangat besar
def stream_jsonl(path)
File.foreach(path).lazy.map { |baris| JSON.parse(baris.chomp, symbolize_names: true) }
end
events = stream_jsonl("events.jsonl")
events.select { |e| e[:event] == "login" }.first(10).each do |e|
puts "#{e[:waktu]}: User #{e[:user_id]} login"
end
gem oj — Parser JSON Tercepat #
oj (Optimized JSON) adalah gem C extension yang jauh lebih cepat dari parser bawaan Ruby:
gem install oj
require 'oj'
# API sama dengan JSON standar
data = Oj.load('{"nama":"Rina","umur":28}') # parse
json = Oj.dump({ nama: "Rina", umur: 28 }) # generate
json = Oj.dump({ nama: "Rina" }, indent: 2) # pretty
# Mode berbeda untuk perilaku berbeda
Oj.load(json_str, mode: :strict) # JSON RFC ketat
Oj.load(json_str, mode: :compat) # kompatibel dengan JSON gem
Oj.load(json_str, mode: :object) # support Ruby object encoding
Oj.load(json_str, mode: :rails) # kompatibel dengan Rails as_json
# Ganti parser default Ruby secara global (untuk Rails)
# Tambahkan di config/initializers/oj.rb:
Oj.optimize_rails # ganti JSON standar dengan oj untuk seluruh Rails
# Benchmark perkiraan:
# JSON.parse: ~500 MB/s
# Oj.load: ~1.5 GB/s (3x lebih cepat)
Kapan perlu oj:
✓ Parsing ribuan request JSON per detik
✓ File JSON besar (> 1 MB)
✓ Aplikasi high-throughput yang bottleneck di JSON parsing
✗ Untuk penggunaan biasa, JSON standar sudah sangat cukup
Validasi dengan JSON Schema #
Untuk memvalidasi struktur JSON yang masuk (misalnya dari request API), gunakan json-schema gem:
gem install json-schema
require 'json-schema'
schema = {
"type" => "object",
"required" => ["nama", "email", "umur"],
"properties" => {
"nama" => {
"type" => "string",
"minLength" => 2,
"maxLength" => 100
},
"email" => {
"type" => "string",
"format" => "email"
},
"umur" => {
"type" => "integer",
"minimum" => 0,
"maximum" => 150
},
"role" => {
"type" => "string",
"enum" => ["user", "admin", "moderator"]
}
},
"additionalProperties" => false # tolak key yang tidak ada di schema
}
# Validasi data
data_valid = { "nama" => "Rina", "email" => "[email protected]", "umur" => 28 }
data_tidak_valid = { "nama" => "R", "email" => "bukan-email", "umur" => -1 }
# Cek valid/tidak
puts JSON::Validator.validate(schema, data_valid) # => true
puts JSON::Validator.validate(schema, data_tidak_valid) # => false
# Dapatkan semua error
error_list = JSON::Validator.fully_validate(schema, data_tidak_valid)
error_list.each { |e| puts "- #{e}" }
# => - The property '#/nama' was shorter than the minimum length of 2
# => - The property '#/email' did not match the 'email' format
# => - The property '#/umur' did not have a minimum value of 0
# Raise exception jika tidak valid
begin
JSON::Validator.validate!(schema, data_tidak_valid)
rescue JSON::Schema::ValidationError => e
puts "Validasi gagal: #{e.message}"
end
Pola JSON di API Rails #
# Controller yang mengembalikan JSON
class Api::V1::ProdukController < ApplicationController
def index
produk = Produk.aktif.limit(params[:per_page] || 20)
render json: {
status: "sukses",
data: produk.map { |p| serialisasi_produk(p) },
meta: {
total: Produk.aktif.count,
halaman: params[:page] || 1,
per_page: params[:per_page] || 20
}
}
end
def show
produk = Produk.find(params[:id])
render json: { status: "sukses", data: serialisasi_produk(produk) }
rescue ActiveRecord::RecordNotFound
render json: { status: "error", pesan: "Produk tidak ditemukan" }, status: :not_found
end
def create
data = JSON.parse(request.body.read, symbolize_names: true)
produk = Produk.new(data.slice(:nama, :harga, :kategori_id))
if produk.save
render json: { status: "sukses", data: serialisasi_produk(produk) }, status: :created
else
render json: { status: "error", errors: produk.errors }, status: :unprocessable_entity
end
rescue JSON::ParserError
render json: { status: "error", pesan: "Request body bukan JSON yang valid" }, status: :bad_request
end
private
def serialisasi_produk(produk)
{
id: produk.id,
nama: produk.nama,
harga: produk.harga,
kategori: produk.kategori&.nama,
tersedia: produk.stok > 0,
created_at: produk.created_at.iso8601
}
end
end
# Middleware untuk parsing JSON body secara otomatis
# (sudah ada di Rails, tapi berguna untuk Sinatra/Rack murni)
class JsonBodyParser
def initialize(app)
@app = app
end
def call(env)
if env["CONTENT_TYPE"]&.include?("application/json")
body = env["rack.input"].read
env["rack.input"].rewind
begin
env["parsed_body"] = JSON.parse(body, symbolize_names: true)
rescue JSON::ParserError
return [400, { "Content-Type" => "application/json" },
['{"error":"Invalid JSON"}']]
end
end
@app.call(env)
end
end
Format dan Konvensi JSON API #
# Struktur response JSON yang konsisten
# Format sukses
{
"status": "sukses",
"data": { ... }, # objek tunggal
"meta": { # opsional — pagination, dll
"total": 100,
"halaman": 1
}
}
# Format error
{
"status": "error",
"kode": "VALIDATION_ERROR",
"pesan": "Input tidak valid",
"errors": [
{ "field": "email", "pesan": "Format email tidak valid" },
{ "field": "umur", "pesan": "Harus lebih dari 0" }
]
}
# Helper untuk response yang konsisten
module ApiResponse
def sukses(data, status: :ok, meta: nil)
payload = { status: "sukses", data: data }
payload[:meta] = meta if meta
render json: payload, status: status
end
def error(pesan, kode: "ERROR", status: :bad_request, errors: nil)
payload = { status: "error", kode: kode, pesan: pesan }
payload[:errors] = errors if errors
render json: payload, status: status
end
end
class ApplicationController < ActionController::API
include ApiResponse
end
Ringkasan #
require 'json'sudah bawaan Ruby — tidak perlu install gem untuk kebutuhan dasar parsing dan generate JSON.symbolize_names: trueuntuk API response — mengaksesdata[:nama]lebih idiomatik daridata["nama"]di Ruby; gunakan secara konsisten.- Selalu tangani
JSON::ParserError— input dari pengguna atau API eksternal bisa berupa JSON tidak valid; jangan biarkan crash.JSON.pretty_generateuntuk file konfigurasi — lebih mudah dibaca manusia; gunakanJSON.generate(tanpa pretty) untuk response API yang mengutamakan ukuran kecil.- Implementasikan
as_jsonbukanto_json—as_jsonmengembalikan Hash yang bisa dikombinasikan dan dikustomisasi;to_jsonhanya mengubahnya menjadi String di akhir.- Jangan sertakan data sensitif dalam serialisasi —
password_hash,api_key, dan sejenisnya tidak boleh masuk ke JSON response; definisikan atribut yang diizinkan secara eksplisit dias_json.- JSON Lines untuk data besar — satu objek per baris memungkinkan streaming dan pemrosesan baris per baris tanpa memuat seluruh file ke memori.
- gem
ojuntuk throughput tinggi — 3x lebih cepat dari parser bawaan; gunakan jika parsing JSON menjadi bottleneck.- JSON Schema untuk validasi input —
json-schemagem memvalidasi struktur dan tipe sebelum data diproses; mencegah bug karena data yang tidak sesuai format.- Konsistensi format response — tentukan satu struktur
{status, data, meta}dan{status, kode, pesan, errors}untuk seluruh API; mudahkan klien menangani response secara generik.