JSON

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: true untuk API response — mengakses data[:nama] lebih idiomatik dari data["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_generate untuk file konfigurasi — lebih mudah dibaca manusia; gunakan JSON.generate (tanpa pretty) untuk response API yang mengutamakan ukuran kecil.
  • Implementasikan as_json bukan to_jsonas_json mengembalikan Hash yang bisa dikombinasikan dan dikustomisasi; to_json hanya mengubahnya menjadi String di akhir.
  • Jangan sertakan data sensitif dalam serialisasipassword_hash, api_key, dan sejenisnya tidak boleh masuk ke JSON response; definisikan atribut yang diizinkan secara eksplisit di as_json.
  • JSON Lines untuk data besar — satu objek per baris memungkinkan streaming dan pemrosesan baris per baris tanpa memuat seluruh file ke memori.
  • gem oj untuk throughput tinggi — 3x lebih cepat dari parser bawaan; gunakan jika parsing JSON menjadi bottleneck.
  • JSON Schema untuk validasi inputjson-schema gem 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.

← Sebelumnya: Mocking   Berikutnya: YAML →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact