JSON di TypeScript #

JSON (JavaScript Object Notation) adalah format pertukaran data yang paling umum di ekosistem TypeScript — digunakan untuk API response, file konfigurasi, penyimpanan data, dan komunikasi antar service. Meskipun JSON.parse dan JSON.stringify terlihat sederhana, ada banyak jebakan yang tidak terlihat di permukaan: JSON.parse mengembalikan any yang menonaktifkan seluruh type checking, beberapa nilai JavaScript tidak bisa di-serialisasi, Date berubah menjadi string setelah roundtrip, dan Map/Set menjadi {} yang kosong. Artikel ini membahas semua aspek JSON di TypeScript secara mendalam — mulai dari penggunaan dasar yang benar, validasi tipe yang ketat, hingga pola-pola yang aman untuk production code.

JSON.stringify — Serialisasi ke String #

JSON.stringify mengubah nilai JavaScript menjadi string JSON. TypeScript mengetahui bahwa fungsi ini mengembalikan string:

// Penggunaan dasar
const pengguna = {
  id: "usr-001",
  nama: "Budi Santoso",
  email: "[email protected]",
  usia: 25,
  aktif: true,
};

const json = JSON.stringify(pengguna);
// '{"id":"usr-001","nama":"Budi Santoso","email":"[email protected]","usia":25,"aktif":true}'

// Parameter kedua: replacer — filter atau transformasi
const jsonTanpaEmail = JSON.stringify(pengguna, (key, value) => {
  if (key === "email") return undefined; // undefined = hapus properti
  return value;
});
// '{"id":"usr-001","nama":"Budi Santoso","usia":25,"aktif":true}'

// Atau gunakan array sebagai whitelist properti
const jsonTerpilih = JSON.stringify(pengguna, ["id", "nama"]);
// '{"id":"usr-001","nama":"Budi Santoso"}'

// Parameter ketiga: indentasi untuk output yang mudah dibaca
const jsonRapi = JSON.stringify(pengguna, null, 2);
// {
//   "id": "usr-001",
//   "nama": "Budi Santoso",
//   ...
// }

Nilai yang Tidak Bisa Di-serialisasi #

Beberapa tipe JavaScript diabaikan atau diubah saat serialisasi:

const dataBermasalah = {
  fungsi: () => "halo",    // ✗ undefined → properti dihapus
  simbol: Symbol("test"),  // ✗ undefined → properti dihapus
  tidakTerdefinisi: undefined, // ✗ properti dihapus
  tanggal: new Date("2025-05-07"), // Diubah ke string ISO
  tak_terhingga: Infinity, // ✗ null
  bukan_angka: NaN,        // ✗ null
  bigint: 9007199254740993n, // ✗ TypeError: Do not know how to serialize a BigInt
};

console.log(JSON.stringify(dataBermasalah));
// '{"tanggal":"2025-05-07T00:00:00.000Z","tak_terhingga":null,"bukan_angka":null}'
// Fungsi, simbol, dan undefined hilang tanpa peringatan!

// Map dan Set kehilangan semua data
const map = new Map([["kunci", "nilai"]]);
const set = new Set([1, 2, 3]);
console.log(JSON.stringify({ map, set })); // '{"map":{},"set":{}}'
JSON.stringify tidak melempar error untuk nilai yang tidak bisa di-serialisasi — ia diam-diam menghapus properti tersebut atau menggantikannya dengan null. Bug ini sangat berbahaya karena tidak terdeteksi saat kompilasi maupun runtime tanpa pengujian eksplisit. Selalu verifikasi output JSON.stringify untuk objek yang mengandung Date, Map, Set, undefined, atau nilai kustom.

JSON.parse — Deserialisasi dan Jebakan any #

JSON.parse adalah sumber utama kehilangan type safety di TypeScript — ia mengembalikan any:

const jsonString = '{"id":"usr-001","nama":"Budi","usia":25}';

// ANTI-PATTERN: Langsung gunakan hasil parse tanpa validasi
const data = JSON.parse(jsonString); // Tipe: any
data.nama.toUpperCase(); // ✓ Tidak ada error TypeScript
data.tidakAda.tidakAda; // ✓ Tidak ada error TypeScript — tapi crash di runtime!

// ANTI-PATTERN: Type assertion tanpa validasi
const pengguna = JSON.parse(jsonString) as Pengguna;
// TypeScript percaya ini Pengguna, tapi tidak ada jaminan di runtime

// BENAR: Validasi dengan Zod sebelum digunakan
import { z } from "zod";

const SchemaPengguna = z.object({
  id: z.string(),
  nama: z.string(),
  usia: z.number().min(0).max(150),
  email: z.string().email().optional(),
});

type Pengguna = z.infer<typeof SchemaPengguna>;

function parseJSON<T>(schema: z.ZodSchema<T>, input: string): T {
  let parsed: unknown;
  try {
    parsed = JSON.parse(input);
  } catch {
    throw new SyntaxError("Input bukan JSON yang valid");
  }

  const hasil = schema.safeParse(parsed);
  if (!hasil.success) {
    throw new TypeError(
      `Struktur JSON tidak sesuai: ${JSON.stringify(hasil.error.flatten())}`
    );
  }
  return hasil.data;
}

// Penggunaan yang aman
const pengguna = parseJSON(SchemaPengguna, jsonString);
// pengguna.nama → string (TypeScript tahu tipenya!)
// pengguna.email → string | undefined

Replacer Function — Serialisasi Kustom #

Replacer function memberikan kontrol penuh atas apa yang disertakan dan bagaimana nilainya diserialisasi:

// Kasus 1: Serialisasi Date sebagai timestamp Unix
function replacerTimestamp(key: string, value: unknown): unknown {
  if (value instanceof Date) {
    return { __type: "Date", value: value.getTime() };
  }
  return value;
}

const data = {
  nama: "Jadwal Sholat",
  dibuat: new Date("2025-05-07T12:00:00Z"),
};

const json = JSON.stringify(data, replacerTimestamp);
// '{"nama":"Jadwal Sholat","dibuat":{"__type":"Date","value":1746619200000}}'

// Kasus 2: Serialisasi Map dan Set
function replacerKoleksi(key: string, value: unknown): unknown {
  if (value instanceof Map) {
    return { __type: "Map", entries: [...value.entries()] };
  }
  if (value instanceof Set) {
    return { __type: "Set", values: [...value.values()] };
  }
  if (typeof value === "bigint") {
    return { __type: "BigInt", value: value.toString() };
  }
  return value;
}

const dataMap = {
  konfigurasi: new Map([["host", "localhost"], ["port", "5432"]]),
  tag: new Set(["typescript", "nodejs"]),
  id: 9007199254740993n,
};

const jsonMap = JSON.stringify(dataMap, replacerKoleksi, 2);

Reviver Function — Deserialisasi Kustom #

Reviver adalah kebalikan dari replacer — ia berjalan saat JSON.parse dan memungkinkan transformasi nilai selama deserialisasi:

// Reviver untuk mengembalikan Date dari string ISO
function reviverTanggal(key: string, value: unknown): unknown {
  if (typeof value === "string") {
    // Cek apakah string adalah format ISO 8601
    const polaISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
    if (polaISO.test(value)) {
      return new Date(value);
    }
  }
  return value;
}

const jsonDenganTanggal = '{"nama":"Acara","mulai":"2025-05-07T08:00:00.000Z"}';
const acara = JSON.parse(jsonDenganTanggal, reviverTanggal);

console.log(acara.mulai instanceof Date); // true
console.log(acara.mulai.getFullYear());   // 2025

// Reviver untuk mengembalikan Map dari representasi kustom
function reviverKoleksi(key: string, value: unknown): unknown {
  if (typeof value === "object" && value !== null) {
    const obj = value as Record<string, unknown>;
    if (obj.__type === "Date" && typeof obj.value === "number") {
      return new Date(obj.value);
    }
    if (obj.__type === "Map" && Array.isArray(obj.entries)) {
      return new Map(obj.entries as [unknown, unknown][]);
    }
    if (obj.__type === "Set" && Array.isArray(obj.values)) {
      return new Set(obj.values as unknown[]);
    }
    if (obj.__type === "BigInt" && typeof obj.value === "string") {
      return BigInt(obj.value);
    }
  }
  return value;
}

Metode toJSON pada Kelas #

Kelas bisa mendefinisikan metode toJSON() yang dipanggil otomatis oleh JSON.stringify. Ini adalah cara idiomatik untuk mengontrol serialisasi kelas:

class Uang {
  constructor(
    private readonly jumlah: number,
    private readonly matauang: string
  ) {}

  // JSON.stringify akan memanggil ini secara otomatis
  toJSON(): { jumlah: number; matauang: string; format: string } {
    return {
      jumlah: this.jumlah,
      matauang: this.matauang,
      format: `${this.matauang} ${this.jumlah.toLocaleString("id-ID")}`,
    };
  }

  static dariJSON(data: { jumlah: number; matauang: string }): Uang {
    return new Uang(data.jumlah, data.matauang);
  }
}

class Produk {
  constructor(
    public readonly id: string,
    public readonly nama: string,
    public readonly harga: Uang,
    private readonly passwordInternal: string // Tidak boleh diserialisasi!
  ) {}

  toJSON() {
    return {
      id: this.id,
      nama: this.nama,
      harga: this.harga, // Uang.toJSON() akan dipanggil secara nested
      // passwordInternal sengaja tidak dimasukkan
    };
  }
}

const produk = new Produk("PRD-001", "Kurma Ajwa", new Uang(85_000, "IDR"), "rahasia");
console.log(JSON.stringify(produk, null, 2));
// {
//   "id": "PRD-001",
//   "nama": "Kurma Ajwa",
//   "harga": {
//     "jumlah": 85000,
//     "matauang": "IDR",
//     "format": "IDR 85.000"
//   }
// }
// passwordInternal tidak muncul!

Validasi JSON Schema dengan Zod #

Zod adalah library validasi schema yang sangat powerful untuk TypeScript. Ia menghasilkan tipe TypeScript secara otomatis dari schema:

import { z } from "zod";

// Schema yang kompleks dengan transformasi
const SchemaTransaksi = z.object({
  id: z.string().uuid("ID harus berformat UUID"),
  jumlah: z.number().positive("Jumlah harus positif"),
  matauang: z.enum(["IDR", "USD", "EUR"]),
  waktu: z.string().datetime().transform((s) => new Date(s)), // string → Date
  metadata: z.record(z.string(), z.unknown()).optional(),
  status: z.enum(["menunggu", "diproses", "selesai", "gagal"]).default("menunggu"),
  item: z.array(z.object({
    produkId: z.string(),
    kuantitas: z.number().int().min(1),
    harga: z.number().positive(),
  })).min(1, "Transaksi harus memiliki minimal 1 item"),
});

type Transaksi = z.infer<typeof SchemaTransaksi>;
// TypeScript tahu waktu bertipe Date (sudah ditransformasi)

// Validasi dengan error yang kaya
const jsonTransaksi = `{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "jumlah": 150000,
  "matauang": "IDR",
  "waktu": "2025-05-07T08:30:00Z",
  "item": [
    { "produkId": "PRD-001", "kuantitas": 2, "harga": 75000 }
  ]
}`;

const hasil = SchemaTransaksi.safeParse(JSON.parse(jsonTransaksi));

if (hasil.success) {
  const transaksi: Transaksi = hasil.data;
  console.log(`Transaksi ${transaksi.id}: ${transaksi.jumlah} ${transaksi.matauang}`);
  console.log(`Waktu: ${transaksi.waktu.toISOString()}`); // waktu adalah Date!
} else {
  console.error("Validasi gagal:", hasil.error.flatten());
}

JSON dari File dan API #

Membaca JSON dari File #

import { readFile } from "fs/promises";
import { z } from "zod";

const SchemaKonfigurasi = z.object({
  database: z.object({
    host: z.string(),
    port: z.coerce.number(),
    nama: z.string(),
  }),
  server: z.object({
    port: z.coerce.number().default(3000),
    env: z.enum(["development", "production", "test"]).default("development"),
  }),
  fitur: z.record(z.string(), z.boolean()).optional(),
});

type Konfigurasi = z.infer<typeof SchemaKonfigurasi>;

async function muatKonfigurasi(filePath: string): Promise<Konfigurasi> {
  let isiFile: string;
  try {
    isiFile = await readFile(filePath, "utf-8");
  } catch {
    throw new Error(`Gagal membaca file konfigurasi: ${filePath}`);
  }

  let dataRaw: unknown;
  try {
    dataRaw = JSON.parse(isiFile);
  } catch {
    throw new SyntaxError(`File ${filePath} bukan JSON yang valid`);
  }

  const hasil = SchemaKonfigurasi.safeParse(dataRaw);
  if (!hasil.success) {
    const errors = hasil.error.flatten().fieldErrors;
    throw new TypeError(
      `Konfigurasi tidak valid:\n${JSON.stringify(errors, null, 2)}`
    );
  }

  return hasil.data;
}

Membaca JSON dari API #

const SchemaResponseAPI = z.object({
  sukses: z.boolean(),
  data: z.array(z.object({
    id: z.string(),
    nama: z.string(),
    harga: z.number(),
  })),
  meta: z.object({
    total: z.number(),
    halaman: z.number(),
    perHalaman: z.number(),
  }),
});

type ResponseAPI = z.infer<typeof SchemaResponseAPI>;

async function ambilProduk(halaman = 1): Promise<ResponseAPI> {
  const response = await fetch(`https://api.example.com/produk?halaman=${halaman}`);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  const json: unknown = await response.json();

  const hasil = SchemaResponseAPI.safeParse(json);
  if (!hasil.success) {
    console.error("Struktur response API tidak sesuai ekspektasi:", hasil.error.flatten());
    throw new TypeError("Format response API tidak valid");
  }

  return hasil.data;
}

JSON Lines — Streaming Data Besar #

JSON Lines (JSONL) adalah format di mana setiap baris adalah satu objek JSON — ideal untuk log files, data exports, dan streaming:

import { createReadStream } from "fs";
import * as readline from "readline";
import { z } from "zod";

const SchemaEntriLog = z.object({
  waktu: z.string(),
  level: z.enum(["debug", "info", "warn", "error"]),
  pesan: z.string(),
  requestId: z.string().optional(),
});

type EntriLog = z.infer<typeof SchemaEntriLog>;

async function* bacaLogStream(filePath: string): AsyncGenerator<EntriLog> {
  const fileStream = createReadStream(filePath, { encoding: "utf-8" });
  const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });

  for await (const baris of rl) {
    if (baris.trim() === "") continue; // Lewati baris kosong

    try {
      const data = JSON.parse(baris);
      const hasil = SchemaEntriLog.safeParse(data);
      if (hasil.success) {
        yield hasil.data;
      }
    } catch {
      // Lewati baris yang bukan JSON valid
    }
  }
}

// Tulis JSONL
function tulisEntriLog(entri: EntriLog): string {
  return JSON.stringify(entri); // Satu objek JSON per baris, tanpa newline
}

// Penggunaan
async function analisisLog(filePath: string): Promise<void> {
  let jumlahError = 0;
  let jumlahTotal = 0;

  for await (const entri of bacaLogStream(filePath)) {
    jumlahTotal++;
    if (entri.level === "error") jumlahError++;
  }

  console.log(`Total: ${jumlahTotal}, Error: ${jumlahError} (${((jumlahError / jumlahTotal) * 100).toFixed(1)}%)`);
}

JSON5 dan JSONC — Untuk File Konfigurasi #

JSON standar sangat ketat — tidak boleh ada komentar, trailing comma, atau nilai khusus. Untuk file konfigurasi yang dibaca manusia, pertimbangkan format yang lebih toleran:

// JSONC (JSON with Comments) — digunakan oleh tsconfig.json, .vscode/settings.json
// Didukung oleh: VS Code, TypeScript, beberapa linter

// tsconfig.json boleh mengandung komentar:
// {
//   "compilerOptions": {
//     "strict": true, // Aktifkan semua strict check
//     "target": "ES2022"
//   }
// }

// JSON5 — superset JSON dengan sintaks yang lebih toleran
// npm install json5
import JSON5 from "json5";

const konfigJSON5 = `{
  // Komentar diizinkan
  host: 'localhost',  // Kunci tanpa kutip diizinkan
  port: 5432,
  // Trailing comma boleh
  ssl: false,
}`;

const konfig = JSON5.parse(konfigJSON5);

// Untuk file konfigurasi proyek, pertimbangkan YAML (lihat artikel YAML)
// yang bahkan lebih mudah dibaca oleh manusia

Alur Kerja JSON yang Aman #

flowchart TD
    A[Data Masuk\nAPI Response / File / Input] --> B[JSON.parse\nstring → unknown]
    B --> C[Validasi dengan Zod\nunknown → Tipe yang Diketahui]
    C --> D{Validasi\nBerhasil?}
    D -- Ya --> E[Gunakan Data\ndengan Type Safety Penuh]
    D -- Tidak --> F[Tangani Error\nLog + Return Error / Throw]

    G[Data TypeScript] --> H{Tipe Spesial?\nDate Map Set BigInt}
    H -- Ya --> I[Replacer Function\natau toJSON Method]
    H -- Tidak --> J[JSON.stringify\nLangsung]
    I --> J
    J --> K[String JSON\nSiap Dikirim / Disimpan]

    style C fill:#339af0,color:#fff
    style D fill:#fcc419,color:#000
    style E fill:#51cf66,color:#fff
    style F fill:#ff6b6b,color:#fff

Ringkasan #

  • JSON.parse mengembalikan any — ini menonaktifkan seluruh type checking; selalu validasi hasil JSON.parse dengan Zod atau type guard sebelum digunakan, jangan langsung type assertion.
  • JSON.stringify diam-diam menghapus undefined, fungsi, dan simbol — properti dengan nilai tersebut hilang tanpa error; selalu verifikasi output untuk object yang mengandung nilai-nilai ini.
  • Date berubah menjadi string ISO setelah JSON.stringify — dan JSON.parse tidak otomatis mengembalikannya ke Date; gunakan reviver function atau transformasi manual untuk roundtrip yang benar.
  • Map dan Set menjadi {} setelah serialisasi — gunakan replacer/reviver kustom atau konversi ke array sebelum serialisasi jika perlu mempertahankan struktur ini.
  • BigInt menyebabkan TypeError saat JSON.stringify — gunakan replacer untuk mengubahnya ke string terlebih dahulu.
  • Definisikan toJSON() di kelas untuk mengontrol serialisasi — ini mencegah properti sensitif (password, token) tidak sengaja tersertakan dalam output JSON.
  • Zod untuk validasi schemaz.infer<typeof Schema> menghasilkan tipe TypeScript secara otomatis dari schema; gunakan safeParse() (bukan parse()) agar error bisa ditangani tanpa try-catch.
  • JSON Lines (JSONL) untuk data besar — satu objek JSON per baris memungkinkan streaming dan pemrosesan bertahap tanpa memuat seluruh file ke memori.
  • Parsing JSON dari file dan API harus selalu divalidasi — data dari sumber eksternal tidak pernah bisa dipercaya tipenya; validasi schema adalah garis pertahanan terakhir sebelum data masuk ke sistem.

← Sebelumnya: Mocking   Berikutnya: YAML →

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