JSON

JSON #

JSON adalah format pertukaran data yang hampir tidak bisa dihindari di dunia pemrograman modern — API, konfigurasi, log terstruktur, dan komunikasi antar service hampir semuanya menggunakan JSON. Ruby menyediakan library JSON sebagai bagian dari standard library, tanpa perlu menginstall gem tambahan. Library ini menyediakan dua operasi fundamental: parsing JSON string menjadi objek Ruby, dan generating JSON string dari objek Ruby. Tapi di balik kesederhanaannya, ada nuansa penting tentang bagaimana Ruby dan JSON saling memetakan tipe data satu sama lain, bagaimana menangani error dengan tepat, dan bagaimana mengintegrasikan JSON dengan kelas custom di aplikasimu.

Parsing: JSON ke Ruby #

JSON.parse mengubah string JSON menjadi objek Ruby. Ini adalah operasi yang paling sering dilakukan saat bekerja dengan respons API atau membaca file konfigurasi.

require "json"

# Parsing dasar
json_string = '{"nama": "Alice", "umur": 30, "aktif": true}'
data = JSON.parse(json_string)
# => {"nama"=>"Alice", "umur"=>30, "aktif"=>true}

data["nama"]    # => "Alice"
data["umur"]    # => 30
data["aktif"]   # => true

# Array JSON
json_array = '[1, 2, 3, "empat", null, true]'
JSON.parse(json_array)
# => [1, 2, 3, "empat", nil, true]

# JSON bersarang
json_nested = '{
  "pengguna": {
    "id": 1,
    "nama": "Bob",
    "alamat": {
      "kota": "Jakarta",
      "kode_pos": "10110"
    },
    "tag": ["ruby", "developer"]
  }
}'

data = JSON.parse(json_nested)
data["pengguna"]["alamat"]["kota"]   # => "Jakarta"
data["pengguna"]["tag"]              # => ["ruby", "developer"]

Pemetaan Tipe Data JSON ↔ Ruby #

# Tabel konversi otomatis saat parsing
json = '{
  "string": "hello",
  "integer": 42,
  "float": 3.14,
  "boolean_true": true,
  "boolean_false": false,
  "null_value": null,
  "array": [1, 2, 3],
  "object": {"key": "value"}
}'

data = JSON.parse(json)

data["string"].class         # => String
data["integer"].class        # => Integer
data["float"].class          # => Float
data["boolean_true"].class   # => TrueClass
data["boolean_false"].class  # => FalseClass
data["null_value"].class     # => NilClass
data["array"].class          # => Array
data["object"].class         # => Hash

symbolize_names — String Key vs Symbol Key #

Secara default, JSON.parse menggunakan string sebagai key Hash. Opsi symbolize_names: true mengubahnya menjadi symbol.

json = '{"nama": "Alice", "umur": 30}'

# Default: key berupa String
data_string = JSON.parse(json)
data_string["nama"]    # => "Alice"
data_string[:nama]     # => nil  (symbol tidak cocok!)

# Dengan symbolize_names: true — key berupa Symbol
data_symbol = JSON.parse(json, symbolize_names: true)
data_symbol[:nama]     # => "Alice"
data_symbol["nama"]    # => nil  (string tidak cocok!)

# Berlaku rekursif untuk nested hash
json_nested = '{"user": {"name": "Bob", "role": "admin"}}'
data = JSON.parse(json_nested, symbolize_names: true)
data[:user][:name]   # => "Bob"
data[:user][:role]   # => "admin"
# Kapan pakai symbolize_names?
# ✓ Ketika bekerja dengan data internal yang dikontrol kamu
# ✓ Ketika key diakses berkali-kali (symbol lebih hemat memori)
# ✗ Ketika key datang dari input eksternal yang tidak terprediksi
# ✗ Ketika key mengandung karakter yang tidak valid untuk symbol (jarang tapi ada)

Generating: Ruby ke JSON #

JSON.generate (atau to_json) mengubah objek Ruby menjadi string JSON.

require "json"

# Hash ke JSON
data = { nama: "Alice", umur: 30, aktif: true }
JSON.generate(data)
# => '{"nama":"Alice","umur":30,"aktif":true}'

# Atau menggunakan to_json
data.to_json
# => '{"nama":"Alice","umur":30,"aktif":true}'

# Array ke JSON
[1, 2, 3, "empat", nil, false].to_json
# => '[1,2,3,"empat",null,false]'

# Nested structure
{
  pengguna: {
    id: 1,
    nama: "Bob",
    tag: ["ruby", "developer"],
    alamat: nil
  }
}.to_json
# => '{"pengguna":{"id":1,"nama":"Bob","tag":["ruby","developer"],"alamat":null}}'

JSON.pretty_generate — Output yang Terbaca #

data = {
  status: "success",
  data: {
    pengguna: [
      { id: 1, nama: "Alice" },
      { id: 2, nama: "Bob" }
    ]
  }
}

# generate: satu baris, compact
JSON.generate(data)
# => '{"status":"success","data":{"pengguna":[{"id":1,"nama":"Alice"},{"id":2,"nama":"Bob"}]}}'

# pretty_generate: multi-baris, indentasi 2 spasi
puts JSON.pretty_generate(data)
# {
#   "status": "success",
#   "data": {
#     "pengguna": [
#       {
#         "id": 1,
#         "nama": "Alice"
#       },
#       {
#         "id": 2,
#         "nama": "Bob"
#       }
#     ]
#   }
# }

# Gunakan pretty_generate untuk file konfigurasi atau output yang dibaca manusia
# Gunakan generate untuk response API, data transfer (lebih kecil ukurannya)

Pemetaan Tipe Data Ruby → JSON #

# Tipe yang didukung langsung
{ kunci: "nilai" }.to_json      # => '{"kunci":"nilai"}'   Hash
[1, 2, 3].to_json               # => '[1,2,3]'              Array
"string".to_json                # => '"string"'             String
42.to_json                      # => '42'                   Integer
3.14.to_json                    # => '3.14'                 Float
true.to_json                    # => 'true'                 TrueClass
false.to_json                   # => 'false'                FalseClass
nil.to_json                     # => 'null'                 NilClass

# Tipe yang perlu perhatian khusus
:symbol.to_json                 # => '"symbol"'   Symbol jadi String!
Time.now.to_json                # => '"2024-01-15 10:30:00 +0700"'  Time jadi String
Date.today.to_json              # => '"2024-01-15"'  Date jadi String

# Integer besar aman di Ruby, tapi perhatikan kompatibilitas JSON parser lain
9_007_199_254_740_993.to_json   # Melebihi Number.MAX_SAFE_INTEGER di JavaScript!

Symbol Ruby dikonversi menjadi String saat di-serialize ke JSON. Ketika kamu parse JSON tersebut kembali, hasilnya adalah String, bukan Symbol — kecuali menggunakan symbolize_names: true. Ini bisa menyebabkan mismatch yang membingungkan jika kamu tidak sadar akan konversi ini.

data = { status: :aktif, id: 1 }
json = data.to_json      # => '{"status":"aktif","id":1}'

parsed = JSON.parse(json)
parsed["status"]         # => "aktif"  (String, bukan Symbol!)
parsed["status"] == :aktif  # => false!

# Jika butuh Symbol tetap konsisten, selalu canonicalize setelah parse
parsed = JSON.parse(json, symbolize_names: true)
parsed[:status].to_sym  # => :aktif

Penanganan Error #

JSON yang datang dari sumber eksternal tidak selalu valid. Selalu tangani error parsing dengan tepat.

require "json"

# JSON.parse melempar JSON::ParserError untuk input tidak valid
begin
  JSON.parse("ini bukan json")
rescue JSON::ParserError => e
  puts "JSON tidak valid: #{e.message}"
end

begin
  JSON.parse('{"key": "value"')   # JSON tidak lengkap
rescue JSON::ParserError => e
  puts "JSON tidak valid: #{e.message}"
end

# String kosong
begin
  JSON.parse("")
rescue JSON::ParserError => e
  puts "JSON kosong"
end

# Pola aman: parse dengan default jika gagal
def parse_json_aman(string, default: nil)
  JSON.parse(string)
rescue JSON::ParserError
  default
end

hasil = parse_json_aman('{"valid": true}')   # => {"valid"=>true}
hasil = parse_json_aman("tidak valid")       # => nil
hasil = parse_json_aman("", default: {})     # => {}

# Pola dengan logging
def parse_json_dengan_log(string, konteks: "unknown")
  JSON.parse(string)
rescue JSON::ParserError => e
  logger.error("JSON parse error di #{konteks}: #{e.message}")
  logger.debug("Input: #{string.truncate(200)}")
  nil
end
# Validasi JSON tanpa parsing penuh
def valid_json?(string)
  JSON.parse(string)
  true
rescue JSON::ParserError
  false
end

valid_json?('{"key": "value"}')   # => true
valid_json?("tidak valid")        # => false
valid_json?("null")               # => true  (null adalah JSON valid!)
valid_json?("42")                 # => true  (number adalah JSON valid!)

Serialisasi Kelas Custom #

Ketika kamu memiliki kelas sendiri dan ingin mengkonversinya ke/dari JSON, ada beberapa pendekatan yang bisa dipilih.

Pendekatan 1: to_json #

require "json"

class Produk
  attr_reader :id, :nama, :harga, :stok

  def initialize(id, nama, harga, stok = 0)
    @id = id
    @nama = nama
    @harga = harga
    @stok = stok
  end

  # Konversi ke Hash dulu, lalu Hash.to_json
  def as_json
    {
      id: @id,
      nama: @nama,
      harga: @harga,
      stok: @stok
    }
  end

  def to_json(*args)
    as_json.to_json(*args)
  end

  # Factory method untuk parse dari JSON
  def self.from_json(json_string)
    data = JSON.parse(json_string, symbolize_names: true)
    new(data[:id], data[:nama], data[:harga], data[:stok])
  end

  def self.from_hash(hash)
    new(hash[:id] || hash["id"],
        hash[:nama] || hash["nama"],
        hash[:harga] || hash["harga"],
        hash[:stok] || hash["stok"] || 0)
  end
end

produk = Produk.new(1, "Laptop Ruby", 15_000_000, 5)
json = produk.to_json
# => '{"id":1,"nama":"Laptop Ruby","harga":15000000,"stok":5}'

# Parse kembali ke objek
produk_restored = Produk.from_json(json)
produk_restored.nama   # => "Laptop Ruby"

Pendekatan 2: JSON.load dengan custom parser #

require "json"

class Pengguna
  attr_accessor :id, :nama, :email, :dibuat_pada

  def initialize(attrs = {})
    @id = attrs["id"] || attrs[:id]
    @nama = attrs["nama"] || attrs[:nama]
    @email = attrs["email"] || attrs[:email]
    @dibuat_pada = attrs["dibuat_pada"] || attrs[:dibuat_pada]
  end

  def to_json(*args)
    {
      "id" => @id,
      "nama" => @nama,
      "email" => @email,
      "dibuat_pada" => @dibuat_pada&.iso8601
    }.to_json(*args)
  end
end

# Dengan JSON.load dan custom create_id
class PenggunaSerializer
  def self.dump(pengguna)
    JSON.generate({
      id: pengguna.id,
      nama: pengguna.nama,
      email: pengguna.email
    })
  end

  def self.load(json_string)
    data = JSON.parse(json_string)
    Pengguna.new(data)
  end
end

Bekerja dengan File JSON #

Skenario umum: membaca konfigurasi dari file JSON atau menyimpan data ke file JSON.

require "json"
require "pathname"

# Membaca dari file
def baca_config(path)
  file = Pathname.new(path)
  raise "File tidak ditemukan: #{path}" unless file.exist?

  JSON.parse(file.read, symbolize_names: true)
rescue JSON::ParserError => e
  raise "File config tidak valid: #{e.message}"
end

config = baca_config("config/settings.json")
config[:database][:host]   # => "localhost"

# Menulis ke file
def simpan_data(data, path)
  file = Pathname.new(path)
  file.dirname.mkpath   # pastikan direktori ada

  file.open("w") do |f|
    f.write(JSON.pretty_generate(data))
  end
end

simpan_data({ versi: "1.0", timestamp: Time.now.iso8601 }, "output/result.json")

# Pola: baca-modifikasi-tulis dengan atomic write
def update_config(path, &blok)
  file = Pathname.new(path)
  data = JSON.parse(file.read, symbolize_names: true)

  blok.call(data)

  tmp = Pathname.new("#{path}.tmp.#{Process.pid}")
  tmp.write(JSON.pretty_generate(data))
  tmp.rename(file)
end

update_config("config/settings.json") do |config|
  config[:versi] = "2.0"
  config[:diperbarui_pada] = Time.now.iso8601
end

Opsi Lanjutan #

require "json"

# max_nesting — batasi kedalaman parsing (default: 100)
# Mencegah stack overflow dari JSON yang sangat bersarang
begin
  sangat_bersarang = "[" * 200 + "]" * 200
  JSON.parse(sangat_bersarang, max_nesting: 10)
rescue JSON::NestingError => e
  puts "Terlalu dalam: #{e.message}"
end

# allow_nan — izinkan NaN dan Infinity (tidak standar JSON)
JSON.generate(Float::NAN)   # => JSON::GeneratorError!

JSON.generate(Float::NAN, allow_nan: true)   # => 'NaN'
JSON.parse("NaN", allow_nan: true)           # => Float::NAN

# Serialisasi dengan custom state
json = JSON.generate(data,
  indent: "  ",          # 2 spasi untuk indentasi
  space: " ",            # spasi setelah : dan ,
  object_nl: "\n",       # newline setelah setiap key-value
  array_nl: "\n"         # newline setelah setiap elemen array
)

# JSON.dump vs JSON.generate
# JSON.dump: lebih permisif, gunakan to_json jika tersedia
# JSON.generate: lebih ketat, butuh tipe yang dikenal

Pola Penggunaan di Aplikasi Nyata #

Response API yang Konsisten #

require "json"

class ApiResponse
  def self.sukses(data, pesan: "OK")
    {
      status: "success",
      pesan: pesan,
      data: data,
      timestamp: Time.now.utc.iso8601
    }.to_json
  end

  def self.error(pesan, kode: 400, detail: nil)
    response = {
      status: "error",
      pesan: pesan,
      kode: kode,
      timestamp: Time.now.utc.iso8601
    }
    response[:detail] = detail if detail
    response.to_json
  end
end

# Penggunaan
ApiResponse.sukses({ pengguna: { id: 1, nama: "Alice" } })
# => '{"status":"success","pesan":"OK","data":{"pengguna":{"id":1,"nama":"Alice"}},...}'

ApiResponse.error("Pengguna tidak ditemukan", kode: 404)
# => '{"status":"error","pesan":"Pengguna tidak ditemukan","kode":404,...}'

Cache JSON dengan Validasi #

require "json"

class JsonCache
  def initialize(path)
    @path = Pathname.new(path)
    @data = load_dari_disk
  end

  def [](key)
    @data[key.to_s]
  end

  def []=(key, value)
    @data[key.to_s] = value
    simpan_ke_disk
  end

  def delete(key)
    @data.delete(key.to_s)
    simpan_ke_disk
  end

  private

  def load_dari_disk
    return {} unless @path.exist?

    JSON.parse(@path.read)
  rescue JSON::ParserError
    {}  # Reset jika file corrupt
  end

  def simpan_ke_disk
    @path.dirname.mkpath
    @path.write(JSON.pretty_generate(@data))
  end
end

cache = JsonCache.new("tmp/cache.json")
cache["token"] = "abc123"
cache["expires"] = Time.now.to_i + 3600
puts cache["token"]   # => "abc123"

Ringkasan #

  • require "json" dulu — tidak seperti Array dan Hash, JSON perlu di-require secara eksplisit sebelum bisa digunakan.
  • JSON.parse untuk parsing, to_json untuk generating — dua operasi paling fundamental; gunakan JSON.pretty_generate untuk output yang terbaca manusia.
  • symbolize_names: true untuk API internal — mengubah key dari String menjadi Symbol saat parsing; berguna untuk akses dengan notasi symbol tapi hindari untuk key dari input eksternal yang tidak terprediksi.
  • Selalu rescue JSON::ParserError — JSON dari sumber eksternal tidak selalu valid; tangani error ini secara eksplisit daripada membiarkan aplikasi crash.
  • Symbol Ruby menjadi String di JSON — saat serialize ke JSON, Symbol otomatis dikonversi ke String; saat parse kembali, hasilnya String bukan Symbol.
  • Implementasikan as_json dan to_json untuk kelas customas_json mengembalikan Hash representasi, to_json mendelegasikan ke as_json; ini pola yang diikuti Rails dan banyak library.
  • Gunakan atomic write untuk file JSON — tulis ke file temporary dulu lalu rename ke target; ini mencegah file corrupt jika proses tiba-tiba berhenti di tengah penulisan.
  • max_nesting untuk proteksi dari input jahat — batasi kedalaman JSON yang diparse untuk mencegah stack overflow dari input yang sengaja dibuat sangat bersarang.

← Sebelumnya: FileUtils   Berikutnya: CSV →

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