Perkara Penting untuk Mereka Bentuk Kontrak Data JSON API
Inti pati: Enam perkara sangat sukar diubah selepas dihantar: sampul respons, format kesalahan, bentuk penomboran halaman, perwakilan ID dan masa, semantik null berbanding ketiadaan, dan evolusi versi.

Kesimpulan utama: kunci sampul respons, format kesalahan, perwakilan ID dan masa, serta semantik null berbanding ketiadaan dalam kontrak sebelum berbincang butiran penomboran halaman dan penamaan; sentiasa balut tahap atas dalam sampul data supaya penomboran halaman dan ID penjejakan boleh ditambah kemudian tanpa memecahkan pemanggil sedia ada.
Tiada jawapan standard dalam reka bentuk API, tetapi ada keputusan "sukar ditarik balik selepas dipilih" yang lebih baik difikirkan sebelum anda menulis baris kod pertama.
1. Sampul respons atau tidak
Tiga gaya:
// Style A: bare response
{ "id": 1, "name": "张三" }
// Style B: data envelope
{ "data": { "id": 1, "name": "张三" } }
// Style C: data + metadata
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
| Gaya | Kebaikan | Keburukan |
|---|---|---|
| Respons kosong | Paling ringkas, satu lapisan kurang untuk bahagian hadapan | Tidak boleh lampirkan metadata; tatasusunan tahap atas membawa risiko perampasan JSON dan evolusi |
| Sampul data | Mudah menambah meta / errors / links |
Satu lagi tahap bersarang, bahagian hadapan mesti menulis res.data.data |
| Data + metadata | Kebolehan cerapan terbaik | Memerlukan persetujuan ketat tentang medan mana yang masuk meta |
Syor: gunakan sampul data secara seragam untuk senarai dan butiran, dan jangan sekali-kali menjadikan tahap atas sebagai tatasusunan. Dengan itu, menambah penomboran halaman atau ID penjejakan kemudian tidak pernah memecahkan kontrak.
2. Format kesalahan: salin RFC 9457
Mencipta struktur kesalahan sendiri ialah punca besar kos kerjasama. RFC 9457 (yang menggantikan RFC 7807) mentakrifkan application/problem+json:
{
"type": "https://bbdrug.com/problems/insufficient-balance",
"title": "余额不足",
"status": 422,
"detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
"instance": "/accounts/A1001/payments/p-889",
"balance": "12.00",
"required": "30.00"
}
| Medan | Maksud | Diperlukan |
|---|---|---|
type |
URI jenis masalah yang stabil | Disyorkan |
title |
Ringkasan pendek yang boleh dibaca manusia | Disyorkan |
status |
Kod status HTTP | Disyorkan |
detail |
Penjelasan khusus apa yang berlaku kali ini | Pilihan |
instance |
Instans sumber yang bermasalah | Pilihan |
| Medan sambungan | Medan perniagaan arbitrari | Pilihan |
Medan sambungan (seperti balance / required di atas) dibenarkan oleh spesifikasi, dan bahagian hadapan boleh menggunakannya untuk mesej tepat dan bukannya menghurai teks detail.
3. Penomboran halaman: kursor mengalahkan offset
// Offset pagination
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }
// Cursor pagination
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
| Dimensi | Penomboran halaman offset | Penomboran halaman kursor |
|---|---|---|
| Kesukaran pelaksanaan | Rendah (LIMIT/OFFSET) |
Sederhana (memerlukan kunci isihan stabil) |
| Prestasi pada halaman dalam | Buruk (semakin besar offset, semakin perlahan) | Baik |
| Apabila data berubah | Pendua atau item hilang | Stabil |
| Lompat ke halaman | Mungkin | Tidak mungkin |
| Memerlukan jumlah kiraan | Memerlukan kiraan tambahan | Tidak diperlukan |
Syor: gunakan penomboran halaman offset untuk jadual pentadbir (ia memerlukan lompatan halaman dan jumlah), dan penomboran halaman kursor untuk suapan infiniti yang menghadap pengguna.
4. ID dan masa
4.1 Sentiasa gunakan string untuk ID
Alasannya dibincangkan dalam artikel tentang jenis: pada bahagian JavaScript, nilai melebihi 2^53 kehilangan ketepatan secara senyap.
Walaupun ID semasa ialah integer auto-inkremen kecil, menggunakan string meninggalkan ruang untuk beralih ke ID snowflake kemudian.
{ "id": "1000000000000000001" }
4.2 Gunakan string RFC 3339 untuk masa
{
"createdAt": "2026-10-30T10:41:45+08:00",
"updatedAt": "2026-10-30T02:41:45Z"
}
- Jangan gunakan nombor cap masa milisaat: ia sukar dibaca, dan unitnya (saat/milisaat) sering tersalah;
- Nyatakan zon masa secara eksplisit: sama ada dengan
Zatau dengan offset; jangan sekali-kali menggunakan "masa setempat tanpa zon masa"; - Namakan medan secara seragam sebagai
xxxAtsupaya jelas sekali pandang bahawa ia titik masa.
5. null dan ketiadaan
Anda mesti memilih satu dan menulisnya ke dalam dokumentasi:
| Pendekatan | Menyatakan "tiada nilai" | Menyatakan "jangan ubah" |
|---|---|---|
| Pendekatan A | Nilai medan ialah null |
Abaikan medan dalam permintaan |
| Pendekatan B | Medan tidak wujud | Abaikan medan dalam permintaan |
Masalah dengan Pendekatan A ialah "diabaikan" dan "dihantar sebagai null" bermaksud perkara berbeza dalam PATCH, yang mudah disalahtafsir;
masalah dengan Pendekatan B ialah JSON.stringify bahagian hadapan menggugurkan medan yang nilainya undefined, yang kebetulan sepadan dengan semantik.
Syor praktikal:
- Gunakan Pendekatan A untuk badan respons (
nulljelas bermaksud tiada nilai, dan bahagian hadapan tidak perlu membezakanundefined); - Gunakan Pendekatan B untuk badan permintaan (terutamanya
PATCH), digabungkan dengan Merge Patch atau JSON Patch untuk menjadikan semantik eksplisit.
6. Penamaan medan dan struktur
| Keputusan | Pilihan biasa | Awas |
|---|---|---|
| Gaya penamaan | camelCase (ekosistem JS) / snake_case (Python, pangkalan data) |
Seragam merentasi projek, jangan campur |
| Nilai enum | Huruf kecil dengan garis bawah, "pending_review" |
Jangan gunakan nombor; ia sukar dibaca |
| Medan boolean | isActive / enabled |
Jangan gunakan status: 0/1 |
| Medan tatasusunan | Kata nama majmuk, items |
Pulangkan [] untuk tatasusunan kosong, jangan sekali-kali null |
| Kedalaman bersarang | Tidak melebihi 3 tahap | Lebih dalam daripada itu, pecahkan atau tawarkan parameter expand |
Memulangkan [] dan bukannya null untuk tatasusunan kosong sangat penting: data.items.map(...) pada bahagian hadapan tidak akan ranap kerana null.
7. Strategi evolusi
| Perubahan | Memecahkan keserasian | Cara mengendalikan |
|---|---|---|
| Tambah medan pilihan | Tidak | Hantar terus |
| Tambah medan wajib | Ya | Tambah nilai lalai, atau lepaskan mengikut versi |
| Buang medan | Ya | Tandakan usang dahulu, perhatikan pemanggil, kemudian buang |
| Tukar jenis medan | Ya | Tambah nama medan baharu dan tulis berganda semasa peralihan |
| Tukar set nilai enum | Bergantung | Klien mesti mempunyai sandaran untuk nilai tidak dikenali |
| Tukar semantik medan | Ya | Layan sebagai medan baharu |
Tiga peraturan disiplin:
- Klien mesti toleran terhadap medan tidak dikenali (jangan kurung diri anda dengan
additionalProperties: false); - Klien mesti mempunyai cabang sandaran untuk nilai enum tidak dikenali;
- Kekalkan medan usang selama sekurang-kurangnya satu versi utama, dan tandakannya dengan
Deprecationdalam pengepala respons atau dokumentasi.
8. Contoh lengkap
{
"data": [
{
"id": "1001",
"name": "JSON 权威指南",
"price": "89.50",
"tags": ["json", "spec"],
"publishedAt": "2026-09-01T00:00:00+08:00",
"author": null
}
],
"page": { "nextCursor": null, "hasMore": false },
"meta": { "requestId": "req-7f3a", "tookMs": 12 }
}
Respons ini mencerminkan semua konvensyen di atas: sampul data, ID string, jumlah sebagai string, cap masa dengan zon masa,
null bermaksud tiada, tatasusunan kosong, penomboran halaman kursor, dan meta untuk maklumat kebolehan cerapan.
9. Senarai semak
- [ ] Tahap atas bukan tatasusunan; gunakan sampul
datasecara seragam; - [ ] Respons kesalahan mengikut RFC 9457 dan termasuk
typeyang stabil; - [ ] Bentuk penomboran halaman sepadan dengan senario perniagaan;
- [ ] ID ialah string, dan masa menggunakan RFC 3339;
- [ ] Semantik
nulldan ketiadaan ditulis ke dalam dokumentasi; - [ ] Gaya penamaan seragam merentasi projek, dan enum menggunakan string huruf kecil;
- [ ] Klien mempunyai sandaran untuk medan tidak dikenali dan nilai enum tidak dikenali;
- [ ] Perubahan memecahkan melalui versi atau peralihan tulis berganda.
FAQ
Bagaimana saya harus mereka bentuk JSON API yang boleh berevolusi?
Balut senarai dan butiran secara seragam dalam sampul data dan jangan sekali-kali menjadikan tahap atas tatasusunan, supaya penomboran halaman dan ID penjejakan boleh ditambah tanpa memecahkan kontrak. Gunakan ID string dan cap masa RFC 3339, dan tulis semantik null berbanding ketiadaan ke dalam dokumentasi. Kekalkan medan baharu sebagai pilihan dan bebas semantik, dan beri klien sandaran untuk medan tidak dikenali dan nilai enum.
Mengapa tidak mencipta format kesalahan tersuai?
Mencipta struktur kesalahan sendiri ialah punca besar kos kerjasama. Salin application/problem+json RFC 9457, kenal pasti masalah dengan type yang stabil, dan benarkan medan sambungan seperti balance dan required, supaya bahagian hadapan boleh membina mesej tepat tanpa menghurai teks detail.
Patutkah setiap endpoint menggunakan penomboran halaman kursor?
Tidak. Jadual pentadbir memerlukan lompatan halaman dan jumlah, jadi penomboran halaman offset lebih sesuai; suapan infiniti yang menghadap pengguna harus menggunakan penomboran halaman kursor, yang kekal stabil pada halaman dalam dan mengelakkan pendua atau jurang apabila data berubah. Pilih berdasarkan sama ada senario memerlukan lompatan halaman dan jumlah.