Membaca JSON

Enam tipe JSON dan nilai-nilai yang menjebak tes.

Sebagian besar API yang akan Anda uji mengirim dan menerima JSON. Ini format teks untuk data terstruktur, dan cukup kecil untuk dipelajari dalam satu sore. Sintaksnya jarang jadi masalah. Masalahnya, JSON membiarkan beberapa hal tidak terdefinisi, dan program yang berbeda mengisi celah itu dengan cara berbeda. Di celah itulah banyak bug berada, jadi pelajaran ini lebih banyak membahasnya daripada sintaksnya.

Enam jenis nilai

Nilai JSON selalu salah satu dari ini:

TipeContohCatatan
Objek{ "key": "value" }Field bernama di dalam kurung kurawal. Key selalu string dalam tanda kutip ganda.
Array[1, 2, 3]Daftar berurutan dalam kurung siku. Item bisa bertipe apa saja, bercampur.
String"hello"Teks dalam tanda kutip ganda. Tanda kutip tunggal tidak diizinkan.
Angka42, -7, 3.14159, 6.022e23Satu tipe untuk bilangan bulat dan desimal. Tanpa nol di depan, tanpa NaN, tanpa Infinity.
Booleantrue, falseHuruf kecil, tanpa tanda kutip.
NullnullHuruf kecil, tanpa tanda kutip. Berarti “tidak ada nilai”.

Itulah seluruh sistem tipenya. Tidak ada tipe tanggal, tidak ada tipe bilangan bulat yang terpisah dari desimal, dan tidak ada cara menambahkan komentar. API ini punya endpoint yang mengembalikan masing-masing satu:

GET/api/edge-cases/types
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis
JSON
{
  "string": "hello",
  "emptyString": "",
  "integer": 42,
  "negativeInteger": -7,
  "float": 3.14159,
  "exponent": 6.022e23,
  "booleanTrue": true,
  "booleanFalse": false,
  "nullValue": null,
  "array": [1, 2, 3],
  "emptyArray": [],
  "object": { "key": "value" },
  "emptyObject": {},
  "arrayOfObjects": [ { "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" } ],
  "nestedArrays": [[1, 2], [3, [4, [5]]]],
  "stringThatLooksLikeNumber": "123",
  "stringThatLooksLikeBoolean": "true",
  "stringThatLooksLikeNull": "null"
}

Lihat tiga field terakhir. "123", "true", dan "null" semuanya string, karena berada dalam tanda kutip. Orang yang membaca response melihat angka, boolean, dan null. Program melihat tiga string. Saat memeriksa response, periksa tipenya juga, bukan hanya nilainya. API yang suatu hari mengembalikan "price": "249.99" dan keesokan harinya "price": 249.99 sudah mengubah kontraknya, meskipun angkanya terlihat sama.

Menunjuk sebuah nilai

Saat menulis tes atau laporan bug, Anda butuh cara untuk menyebut nilai mana yang Anda maksud. Ini sebagian produk dari pelajaran pertama:

JSON
{
  "id": 1,
  "title": "Wireless Noise-Cancelling Headphones",
  "price": 249.99,
  "category": "electronics",
  "rating": { "rate": 4.3, "count": 127 },
  "inStock": true,
  "createdAt": "2025-06-12T10:30:00Z"
}

Notasi informal yang umum memakai titik untuk masuk ke objek, dan kurung siku berisi angka untuk memilih item dari array. Hitungan dimulai dari nol.

PathNilai
title"Wireless Noise-Cancelling Headphones"
rating.rate4.3
rating.count127

Saat seluruh response berupa array, seperti pada /api/Products?limit=2, ID produk pertama adalah [0].id dan rating produk kedua adalah [1].rating.rate. Setiap alat menulisnya sedikit berbeda. JSONPath, yang akan Anda lihat di Postman, REST Assured, dan banyak library tes, diawali tanda dolar untuk root: $.rating.rate, $[0].id. Ia juga punya wildcard, jadi $[*].priceberarti "harga setiap item". JMESPath, yang dipakai alat baris perintah AWS, menulis hal yang sama sebagai [*].price. Anda belum perlu mempelajari keduanya secara mendalam. Mengetahui bahwa $[0].idberarti "id item pertama" sudah cukup untuk membaca sebagian besar kode tes.

Null, hilang, dan kosong

Ketiganya terlihat mirip tetapi berarti berbeda. Misalkan seorang pengguna tidak punya nama tengah. API bisa menyatakannya setidaknya dengan lima cara:

  • "middleName": null, field-nya ada dan tidak bernilai
  • tidak ada key middleName sama sekali
  • "middleName": "", string kosong
  • "middleName": "null", kata null empat huruf, biasanya bug saat sesuatu memanggil toString pada null
  • "middleName": "None" atau "undefined", bug yang sama dari Python atau JavaScript

Endpoint ini mengembalikan semuanya berdampingan:

GET/api/edge-cases/nulls
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis
JSON
{
  "nullValue": null,
  "nestedNull": { "a": null, "b": { "c": null } },
  "arrayOfNulls": [null, null, null],
  "arrayWithNull": [1, null, 3],
  "nullAsString": "null",
  "undefinedAsString": "undefined",
  "noneAsString": "None",
  "emptyString": "",
  "emptyArray": [],
  "emptyObject": {},
  "zero": 0,
  "false": false
}

Mana yang benar tergantung dokumentasi API, dan dokumentasi sering tidak menyebutkannya. Yang bisa Anda uji adalah konsistensi. Jika satu produk punya "discount": null dan produk lain tidak punya key discount, klien yang membaca product.discount.amount akan crash pada salah satunya dan tidak pada yang lain. Waspadai juga 0 dan falseyang dipakai untuk berarti "tidak diketahui". Stok 0 dan stok yang tidak diketahui siapa pun adalah fakta yang berbeda.

Angka

JSON tidak membatasi ukuran atau presisi angka. Program yang membaca JSON-lah yang membatasinya. JavaScript menyimpan setiap angka sebagai float 64-bit, yang hanya bisa menyimpan bilangan bulat secara persis sampai 9.007.199.254.740.991. Di atas itu, ia membulatkan diam-diam.

GET/api/edge-cases/numbers
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis
JSON (dipangkas)
{
  "int64Max": 9223372036854775807,
  "int64MaxAsString": "9223372036854775807",
  "maxSafeInteger": 9007199254740991,
  "beyondSafeInteger": 9007199254740993,
  "beyondSafeIntegerAsString": "9007199254740993",
  "pointOnePlusPointTwo": 0.30000000000000004,
  "money": 19.99,
  "moneyAsString": "19.99",
  "trailingZeros": 1.50,
  "negativeZero": -0,
  "numberAsString": "42",
  "nanAsString": "NaN"
}

Parse response itu di JavaScript dan di Python, lalu bandingkan:

JavaScript
JSON.parse(body).beyondSafeInteger   // 9007199254740992
JSON.parse(body).int64Max            // 9223372036854776000
Python
json.loads(body)["beyondSafeInteger"]   # 9007199254740993
json.loads(body)["int64Max"]            # 9223372036854775807

JavaScript mengubah 9007199254740993 menjadi 9007199254740992 tanpa error apa pun. Python mempertahankannya persis. Jika sebuah API memakai ID bilangan bulat 64-bit (ID Twitter dulu begitu, dan banyak database juga), klien JavaScript bisa berakhir meminta data yang salah. Itulah sebabnya API yang cermat mengirim ID besar sebagai string, seperti int64MaxAsString di atas. Saat menguji API dengan ID numerik besar, periksa apa yang benar-benar diterima klien di browser.

Desimal punya masalah serupa. 0.1 + 0.2 hasilnya 0.30000000000000004 di hampir semua bahasa, karena float biner tidak bisa merepresentasikan 0.1 secara persis. Untuk uang, ini penting. Ada API yang mengirim harga sebagai angka (19.99), ada yang sebagai string ("19.99"), dan ada yang sebagai sen bulat (1999). Semuanya bisa berfungsi. Tes sebaiknya memeriksa bahwa total cocok sampai ke sen, bukan bahwa hasilnya "cukup dekat".

Dua hal kecil dari response yang sama. 1.50 dan 1.5 adalah angka yang sama setelah di-parse, jadi tes yang membandingkan teks mentah akan gagal di tempat tes yang membandingkan nilai lulus. Dan -0 adalah JSON yang valid, tetapi JSON.stringify(-0) di JavaScript menulis 0, jadi nilai itu tidak bertahan dalam perjalanan bolak-balik.

Tanggal adalah string

JSON tidak punya tipe tanggal, jadi tanggal dikirim sebagai string atau angka, dan setiap API memilih formatnya sendiri. Ini satu momen yang sama, ditulis dengan beberapa cara:

GET/api/edge-cases/dates
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis
JSON (dipangkas)
{
  "iso8601Utc": "2025-07-15T09:30:00Z",
  "iso8601PositiveOffset": "2025-07-15T15:00:00+05:30",
  "iso8601NoZone": "2025-07-15T09:30:00",
  "dateOnly": "2025-07-15",
  "unixSeconds": 1752571800,
  "unixMilliseconds": 1752571800000,
  "usFormat": "07/15/2025",
  "euFormat": "15/07/2025",
  "invalidLeapDay": "2025-02-29T00:00:00Z",
  "nullDate": null,
  "emptyDate": ""
}

Format yang paling sering Anda lihat adalah ISO 8601 dengan zona waktu, seperti 2025-07-15T09:30:00Z. Z berarti UTC. Hal yang perlu diperiksa saat API mengembalikan tanggal:

  • Zona waktunya ada. 2025-07-15T09:30:00 tanpa zona berarti pukul 9.30 di suatu tempat, dan setiap klien akan menebak tempat yang berbeda.
  • Unix timestamp menyebutkan apakah dalam detik atau milidetik. 1752571800 dan 1752571800000 adalah momen yang sama, dan tertukar keduanya membawa Anda ke tahun 1970 atau tahun 57500.
  • 07/15/2025 dan 15/07/2025 tidak ambigu hanya karena 15 tidak mungkin bulan. 03/04/2025 bisa Maret atau April, tergantung siapa yang membacanya.
  • Tanggal mustahil seperti 2025-02-29 diterima sebagian parser dan ditolak yang lain. Coba kirim ke API yang menerima tanggal sebagai input.

Urutan key dan key duplikat

Key pada objek tidak punya urutan. {"a": 1, "b": 2} dan {"b": 2, "a": 1} adalah objek yang sama. Sebagian besar server kebetulan selalu mengirim key dalam urutan yang sama, jadi ada godaan untuk membandingkan seluruh response sebagai string. Tes itu akan rusak pada hari pembaruan library mengubah urutan key, padahal tidak ada yang salah. Bandingkan nilai yang sudah di-parse. Urutan array, sebaliknya, penting: [1, 2] dan [2, 1] adalah array yang berbeda.

Key duplikat adalah kasus yang lebih aneh. Standar JSON mengatakan key "sebaiknya" unik, bukan harus, jadi ini secara teknis diizinkan:

GET/api/edge-cases/duplicate-keys
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis
JSON
{
  "id": 1,
  "name": "first value",
  "name": "second value",
  "nested": { "flag": true, "flag": false },
  "id": 2
}

JSON.parse di JavaScript dan json.loads di Python sama-sama menyimpan nilai terakhir, menghasilkan {"id": 2, "name": "second value", "nested": {"flag": false}}. Parser lain menyimpan nilai pertama atau menolak dokumennya. Jika gateway memeriksa key role yang pertama dan backend membaca yang kedua, perbedaan itu menjadi celah keamanan. Anda tidak akan sering bertemu key duplikat di response, tetapi layak dikirim dalam request saat Anda menguji penanganan input.

Saat JSON-nya rusak

Kadang server mengirim sesuatu yang sama sekali bukan JSON valid: body yang terpotong, halaman error HTML, stack trace. Ini response yang mengaku JSON padahal bukan:

Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id": 1, "name": "Alice", "tags": ["a", "b",], "active": tru

Ada koma berlebih setelah "b", dan body-nya berhenti di tengah true. Statusnya 200 dan header Content-Type berbunyi application/json, jadi klien akan mencoba mem-parse-nya lalu gagal. Node.js melaporkan Unexpected token ']'. Python melaporkan Illegal trailing comma before end of array. Di API sungguhan, ini biasanya berarti server crash di tengah penulisan response, atau proxy memotongnya. Pelajarannya untuk tes: parse body di setiap pemeriksaan. 200 dengan body yang tidak bisa di-parse adalah kegagalan, dan tes yang hanya melihat kode status akan melewatkannya.

Membaca response yang besar

Response sungguhan sering berisi ratusan baris. Sebelum memeriksa detail, pahami bentuknya: apakah level teratas berupa objek atau array, berapa jumlah item-nya, field apa saja yang dimiliki satu item. Lalu baca satu item secara lengkap. Sebagian besar bug JSON terlihat dari bentuknya saja, seperti daftar yang kembali sebagai satu objek saat hasilnya hanya satu, atau objek yang dibungkus key data tambahan di satu endpoint tetapi tidak di endpoint lain.

Latihan

Kirim GET /api/Countries/code/INmasuk untuk melihat dan jawab pertanyaan ini dari response-nya, dengan menulis setiap path memakai notasi titik dari pelajaran ini:

  • Apa tipe population, dan apa tipe callingCode? Mengapa keduanya bisa berbeda?
  • Apa path ke simbol mata uang?
  • Apa path ke bahasa kedua?
  • Field flag berupa emoji. Lihat response mentahnya dengan curl dan bandingkan dengan yang ditampilkan browser Anda. Apa yang berbeda, dan apakah itu penting bagi klien?

Lalu buka /api/edge-cases/stringsmasuk untuk melihat dan temukan setidaknya dua nilai yang terlihat baik-baik saja di browser tetapi bisa merusak program yang menyimpan atau menampilkannya.

Berlatih dan melangkah lebih jauh