JSON Toolkit

Ключові моменти проєктування контрактів даних JSON API

Головне: Шість речей дуже важко змінити після релізу: обгортки відповідей, формати помилок, форма пагінації, подання ID і часу, семантика null проти відсутності та еволюція версій.

Середній~13 хв читанняОпубліковано 2026-10-30Оновлено 2026-10-30
Обкладинка статті «Ключові моменти проєктування контрактів даних JSON API»

Коротко: закріпіть обгортку відповіді, формат помилок, подання ID і часу та семантику null проти відсутності в контракті, перш ніж сперечатися про деталі пагінації й найменування; завжди обгортайте верхній рівень в обгортку data, щоб пагінацію та ID трасування можна було додати пізніше, не ламаючи наявних викликачів.

В проєктуванні API немає стандартних відповідей, але є рішення, які «важко забрати назад після вибору», і їх краще продумати до написання першого рядка коду.

1. Обгортати відповідь чи ні

Три стилі:

// Стиль A: гола відповідь
{ "id": 1, "name": "张三" }

// Стиль B: обгортка data
{ "data": { "id": 1, "name": "张三" } }

// Стиль C: data + метадані
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
Стиль Переваги Недоліки
Гола відповідь Найпростіше, на один шар менше для фронтенду Не можна долучити метадані; масив верхнього рівня несе ризики викрадення JSON і еволюції
Обгортка data Легко додати meta / errors / links Ще один рівень вкладеності, фронтенд має писати res.data.data
Data + метадані Найкраща спостережуваність Потрібна строга домовленість, які поля належать meta

Рекомендація: використовуйте обгортку data однаково і для списків, і для деталей, і ніколи не робіть верхній рівень масивом. Так додавання пагінації чи ID трасування пізніше ніколи не ламає контракт.

2. Формат помилок: копіюйте RFC 9457

Винаходити власну структуру помилок — величезне джерело ціни співпраці. RFC 9457 (що замінює RFC 7807) визначає 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"
}
Поле Значення Обовʼязкове
type Стабільний URI типу проблеми Рекомендовано
title Короткий зрозумілий людині підсумок Рекомендовано
status Код статусу HTTP Рекомендовано
detail Конкретне пояснення, що сталося цього разу Необовʼязково
instance Примірник ресурсу, у якого була проблема Необовʼязково
Поля розширення Довільні бізнес-поля Необовʼязково

Поля розширення (як-от balance / required вище) дозволені специфікацією, і фронтенд може використовувати їх для точних повідомлень замість розбору тексту detail.

3. Пагінація: курсор краще за зміщення

// Пагінація зі зміщенням
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }

// Пагінація за курсором
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
Вимір Пагінація зі зміщенням Пагінація за курсором
Складність реалізації Низька (LIMIT/OFFSET) Середня (потрібен стабільний ключ сортування)
Продуктивність на глибоких сторінках Погана (що більше зміщення, то повільніше) Добра
Коли дані змінюються Дублікати або пропущені елементи Стабільно
Перехід на сторінку Можливий Неможливий
Потрібна загальна кількість Потребує додаткового підрахунку Не потрібна

Рекомендація: використовуйте пагінацію зі зміщенням для адмінських таблиць (їм потрібні переходи на сторінки й підсумки), а пагінацію за курсором — для нескінченних стрічок для користувачів.

4. ID і час

4.1 Завжди використовуйте рядки для ID

Обґрунтування розглянуто в статті про типи: на боці JavaScript значення понад 2^53 мовчки втрачають точність. Навіть якщо поточний ID — маленьке автоінкрементне ціле, використання рядків залишає місце для переходу на ID-сніжинки пізніше.

{ "id": "1000000000000000001" }

4.2 Використовуйте рядки RFC 3339 для часу

{
  "createdAt": "2026-10-30T10:41:45+08:00",
  "updatedAt": "2026-10-30T02:41:45Z"
}
  • Не використовуйте числові мітки часу в мілісекундах: їх важко читати, а одиницю (секунди/мілісекунди) часто плутають;
  • Вказуйте часовий пояс явно: або через Z, або через зміщення; ніколи не використовуйте «локальний час без часового поясу»;
  • Називайте поля однаково як xxxAt, щоб одразу було видно, що це момент часу.

5. null і відсутність

Треба обрати одне й записати в документацію:

Підхід Вираження «значення немає» Вираження «не змінювати»
Підхід A Значення поля — null Пропустити поле в запиті
Підхід B Поля не існує Пропустити поле в запиті

Проблема підходу A в тому, що «пропущено» й «надіслано як null» означають різне в PATCH, що легко неправильно витлумачити; проблема підходу B у тому, що JSON.stringify на фронтенді відкидає поля зі значенням undefined, що випадково збігається із семантикою.

Практична рекомендація:

  • Використовуйте підхід A для тіл відповідей (null явно означає «значення немає», і фронтенду не треба розрізняти undefined);
  • Використовуйте підхід B для тіл запитів (особливо PATCH), у поєднанні з Merge Patch або JSON Patch, щоб семантика була явною.

6. Найменування полів і структура

Рішення Поширені варіанти Застереження
Стиль найменування camelCase (екосистема JS) / snake_case (Python, бази даних) Однаково в усьому проєкті, не змішувати
Значення переліків Нижній регістр з підкресленнями, "pending_review" Не використовуйте числа; їх важко читати
Булеві поля isActive / enabled Не використовуйте status: 0/1
Поля-масиви Іменники в множині, items Повертайте [] для порожніх масивів, ніколи null
Глибина вкладеності Не більше 3 рівнів Глибше — розділіть або запропонуйте параметр expand

Повернення [] замість null для порожніх масивів багато значить: data.items.map(...) на фронтенді не зазнає краху через null.

7. Стратегія еволюції

Зміна Порушує сумісність Як обробити
Додати необовʼязкове поле Ні Випускайте безпосередньо
Додати обовʼязкове поле Так Додайте значення за замовчуванням або випускайте за версією
Видалити поле Так Спершу позначте застарілим, спостерігайте за викликачами, потім видаляйте
Змінити тип поля Так Додайте нову назву поля й подвійний запис під час переходу
Змінити набір значень переліку Залежить Клієнт має мати запасний варіант для невідомих значень
Змінити семантику поля Так Трактуйте як нове поле

Три правила дисципліни:

  1. Клієнти мають бути толерантними до невідомих полів (не замикайте себе в additionalProperties: false);
  2. Клієнти мають мати запасну гілку для невідомих значень переліків;
  3. Тримайте застарілі поля принаймні один мажорний реліз і позначайте їх через Deprecation у заголовках відповідей або документації.

8. Повний приклад

{
  "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 }
}

Ця відповідь відображає всі конвенції вище: обгортку data, ID-рядки, суми рядками, часові мітки з часовим поясом, null, що означає відсутність, порожній масив, пагінацію за курсором і meta для інформації спостережуваності.

9. Чек-лист

  • [ ] Верхній рівень не є масивом; використовуйте обгортку data однаково;
  • [ ] Відповіді про помилки слідують RFC 9457 і містять стабільний type;
  • [ ] Форма пагінації відповідає бізнес-сценарію;
  • [ ] ID — рядки, а час використовує RFC 3339;
  • [ ] Семантика null і відсутності записана в документації;
  • [ ] Стиль найменування однаковий у проєкті, а переліки — рядки в нижньому регістрі;
  • [ ] Клієнт має запасні варіанти для невідомих полів і невідомих значень переліків;
  • [ ] Порушні зміни проходять через версіювання або перехід із подвійним записом.

Часті питання

Як проєктувати еволюційний JSON API?

Обгортайте списки й деталі однаково в обгортку data і ніколи не робіть верхній рівень масивом, щоб пагінацію та ID трасування можна було додати, не ламаючи контракт. Використовуйте ID-рядки й часові мітки RFC 3339 та запишіть семантику null проти відсутності в документацію. Тримайте нові поля необовʼязковими й семантично незалежними та давайте клієнтам запасні варіанти для невідомих полів і значень переліків.

Чому не винаходити власний формат помилок?

Винаходити власну структуру помилок — величезне джерело ціни співпраці. Копіюйте application/problem+json RFC 9457, ідентифікуйте проблеми через стабільний type і дозволяйте поля розширення, як-от balance і required, щоб фронтенд міг будувати точні повідомлення без розбору тексту detail.

Чи має кожна кінцева точка використовувати пагінацію за курсором?

Ні. Адмінські таблиці потребують переходів на сторінки й підсумків, тож пагінація зі зміщенням підходить краще; нескінченні стрічки для користувачів мають використовувати пагінацію за курсором, яка стабільна на глибоких сторінках і уникає дублікатів чи прогалин, коли дані змінюються. Обирайте залежно від того, чи сценарій потребує переходів на сторінки й підсумків.