JSON Toolkit

Вопросы о командной работе: контракты, конфигурация и фикстуры

Главное: Стиль именования, форматы времени, семантика null, формы перечислений, форматы конфигураций, организация фикстур и конфликты слияния — двенадцать компромиссов, о которых команды действительно спорят.

Средний~12 мин чтенияОпубликовано 2026-10-30Обновлено 2026-10-30
Обложка вопросов о командной работе

Главное: договоритесь об этом один раз в командном соглашении — оболочка {"data":…,"meta":…} с HTTP-кодами статуса, единый стиль именования по всему сайту, метки времени RFC 3339 с часовым поясом и строковые перечисления — затем развивайте каждый новый API по одному контракту, только добавляя поля, никогда не меняя и не удаляя их.

На эти вопросы нет единственного правильного ответа, но у команды должен быть единственный ответ — иначе каждый новый API заново начинает тот же спор.

1. Контракты API

Какую структуру должен возвращать API?

Три распространённые формы, у каждой своя цена:

Форма Пример Плюсы Минусы
Сырые данные [{...}, {...}] Просто Не может нести метаданные
Фиксированная оболочка {"code":0,"message":"ok","data":{...}} Единообразная обработка ошибок Бизнес-код дублирует HTTP-статус
Ресурсная оболочка {"data":{...},"meta":{...},"links":{...}} Расширяемая, ясная семантика Один лишний уровень вложенности

Для внутренних бизнес-API предпочитайте «ресурсную оболочку + HTTP-коды статуса, выражающие результат»:

{
  "data": [{ "id": "1", "name": "示例" }],
  "meta": { "total": 128, "page": 2, "pageSize": 20 }
}

Для ошибок используйте HTTP-код статуса плюс структурированное тело ошибки, вместо вложения ещё одного code:

{
  "error": {
    "type": "validation_failed",
    "message": "请求参数校验失败",
    "details": [
      { "path": "/email", "reason": "格式不正确" }
    ]
  }
}

Ключевой момент: HTTP-коды статуса уже выражают успех или неудачу, поэтому бизнес-код избыточен. Когда две системы сосуществуют, несогласованности вроде «201, но code равен 500» почти неизбежны.

Избегайте голых массивов. С массивом на верхнем уровне вы не можете прикрепить пагинацию, итоги или курсоры, а добавление их позже означает изменение каждого вызывающего.

Имена полей должны использовать snake_case или camelCase?

Выберите одно, применяйте по всему сайту и никогда не смешивайте внутри одного JSON-документа.

Стиль Типичная экосистема Пример
camelCase JavaScript / TypeScript / Java userName, createdAt
snake_case Python / Ruby / Go (частично) / столбцы БД user_name, created_at
kebab-case Редко; к ключам с дефисами неудобно обращаться user-name

Критерии:

  1. Кто потребляет этот JSON: выбирайте camelCase, когда доминирует фронтенд, snake_case, когда доминируют конвейеры данных и сервисы Python;
  2. Отображается ли он напрямую на столбцы БД: snake_case экономит слой конвертации при прямой передаче структур таблиц;
  3. Непрерывность существующих систем: сохраняйте то, что использует существующий API; согласованность важнее правильности.

Если действительно нужно конвертировать, делайте это на границе (шлюз или слой запросов фронтенда), а не разбрасывайте вызовы toCamelCase по бизнес-коду.

В каком формате должны быть поля времени?

Используйте строки RFC 3339 / ISO 8601 со смещением часового пояса:

{
  "createdAt": "2026-10-30T11:37:57+08:00",
  "updatedAt": "2026-10-30T03:37:57Z"
}

Ключевые моменты:

  • Указывайте часовой пояс: 2026-10-30 11:37:57 без зоны неизбежно сломается в сценариях с несколькими часовыми поясами;
  • Используйте Z или смещение последовательно: оба корректны, но не смешивайте их внутри одного API;
  • Избегайте числовых меток времени: секунды это или миллисекунды, и включены ли високосные секунды, — каждой команде приходится переосмысливать это. Когда нужна компактность, используйте целое в миллисекундах, но зафиксируйте единицу в документации.

Предпочитайте имена полей с суффиксом At, например createdAt / updatedAt / deletedAt, чтобы с первого взгляда было очевидно, что значение — точка во времени, а не длительность. Для длительностей используйте timeoutMs, ttlSeconds — помещайте единицу в имя.

Должны ли null быть null или опущенным полем?

Зависит от того, «имеет ли это поле смысл»:

Семантика Форма Примечания
Поле существует, но ещё не имеет значения "name": null Явно говорит «проверено, и оно пусто»
Поле не относится к этому ресурсу Опустить Например, у обычного пользователя не должно быть vipExpireAt
Частичное обновление (PATCH) Опустить = не менять Здесь только null означает «очистить»

Третий случай чаще всего упускается. В PATCH-запросе:

{ "nickname": null }

это означает «очистить никнейм», тогда как отсутствие nickname в теле запроса означает «не трогать никнейм». Если трактовать оба как «очистить», пользователь, меняющий email, сотрёт свой никнейм.

Ещё раз: не используйте undefined. Это не валидное значение JSON, и JSON.stringify полностью отбрасывает ключ, поэтому поведение совпадает с «опустить», хотя код выглядит как «явно передано пустое значение».

Как называть булевы поля?

Используйте префиксы is / has / can / should и избегайте отрицаний.

{
  "isActive": true,
  "hasPassword": false,
  "canEdit": true
}

Три урока:

  1. Без отрицаний: isDisabled: false заставляет читателя инвертировать один раз; isEnabled: true более прямолинейно. Двойное отрицание (isNotDisabled) — катастрофа;
  2. Не используйте числа для булевых: "enabled": 1 заставляет каждого потребителя писать === 1 один раз;
  3. Не используйте строки: "enable": "true" истинно в JavaScript, и if (data.enable) также истинно для "false"; такие баги отследить крайне трудно.

Поля перечислений должны использовать числа или строки?

Используйте строки, если нет явного ограничения хранения или пропускной способности.

{ "status": "active" }

По сравнению с { "status": 1 } строки выигрывают, потому что:

  • Логи, захваты пакетов и базы данных читаемы напрямую без справочной таблицы;
  • Добавление значения перечисления не меняет смысл существующих значений (вставка значения в середину числового перечисления сдвигает всё);
  • Фронтенду не нужно синхронно поддерживать отображение 1 → 'active'.

Цена — несколько лишних байтов, что после сжатия почти незначительно.

Сопутствующая практика: используйте строчные буквы + подчёркивания для строковых значений перечисления (pending_review), и ограничьте их через enum в Schema, чтобы недопустимые значения отклонялись на этапе валидации.

2. Конфигурация и фикстуры

Конфигурация должна быть в JSON, YAML или TOML?

Выбирайте по назначению, а не по предпочтению:

Формат Хорош для Плох для
JSON Программного чтения и записи, строго структурированных данных, валидации по Schema Частого ручного редактирования (комментарии не разрешены)
YAML Глубоко вложенной конфигурации развёртывания (K8s) Написанные вручную отступы подвержены ошибкам; yes/no превращаются в булевы
TOML Плоской конфигурации зависимостей и инструментов (Cargo.toml, pyproject.toml) Плохой читаемости при глубокой вложенности
JSONC Конфигурации редактора, где нужны комментарии (tsconfig.json) Пересечения границ систем

Частая ошибка в командах — хранить бизнес-конфигурацию в YAML — неявное преобразование типов в YAML (1.0 становится числом, NO становится булевым, 2026-10-30 становится датой) даёт разные результаты в разных парсерах.

Если конфигурация должна строго проверяться программой, выбирайте JSON + Schema, а комментарии документируйте через поле _comment или отдельную документацию.

Как организовывать тестовые фикстуры?

Организуйте по принципу «один базовый вариант + минимальные различия», а не копируя огромный JSON для каждого тестового случая.

fixtures/
├─ users/
│  ├─ base.json              # Базовый объект пользователя
│  ├─ no-email.json          # Отсутствует только email
│  └─ invalid-age.json       # Только age вне диапазона
└─ fixtures.ts               # Утилиты для загрузки и поверхностного слияния
import baseUser from './users/base.json';

export function makeUser(overrides: Partial<User> = {}): User {
  return { ...baseUser, ...overrides };
}

Так новый тестовый случай пишет только различающиеся поля, а изменение базового поля автоматически обновляет все случаи.

Три правила:

  1. Фикстуры должны проходить валидацию по Schema: базовая фикстура сама должна быть валидными данными;
  2. Не экспортируйте реальные данные из продакшена: телефоны, номера удостоверений и адреса — всё это персональная информация;
  3. Фикстуры принадлежат системе контроля версий, но должны оставаться небольшими: отдельный файл больше нескольких десятков КБ заслуживает сокращения или генерации.

Как уменьшить конфликты слияния в JSON-файлах?

JSON конфликтует легко: форматтер перестраивает весь файл сразу, поэтому два человека, каждый меняющий одну строку, конфликтуют по всему файлу.

Четыре тактики:

  1. Унифицируйте форматирование и добавьте его в .editorconfig: согласованные отступы, окончания строк и завершающий перевод строки избегают посторонних различий;
  2. Упорядочивайте ключи: договоритесь об алфавитном порядке, чтобы новое поле не сталкивалось с точкой вставки другого;
  3. Разделяйте файлы: разбейте один гигантский config.json на несколько файлов по доменам, и поверхность конфликтов естественно уменьшится;
  4. Избегайте коммитов «переформатировать всё»: разделяйте форматирование и бизнес-изменения на два коммита, чтобы ревью оставалось читаемым.

Если команда использует lock-файлы (pnpm-lock.yaml, package-lock.json), не слияйте их вручную; регенерируйте вместо этого:

git checkout --theirs pnpm-lock.yaml
pnpm install

Как поддерживать совместимость версий?

Принцип — «добавлять, не менять, не удалять».

Изменение Совместимость Подход
Добавить необязательное поле Совместимо Добавить; потребители игнорируют неизвестные поля
Добавить обязательное поле Несовместимо Поднять версию или предоставить значение по умолчанию
Удалить поле Несовместимо Сначала пометить устаревшим, следить за вызывающими, затем удалить
Изменить тип поля Несовместимо Добавить новое имя поля и двойную запись во время перехода
Изменить набор значений перечисления Зависит Добавление значений совместимо; удаление — нет

Механизмы поддержки:

  • Потребители используют «снисходительный парсинг»: игнорируют неизвестные поля и подставляют значения по умолчанию для отсутствующих;
  • Сервер некоторое время хранит старые поля и возвращает в ответе заголовок Deprecation как подсказку;
  • В Schema не помечайте новые поля как required, а устаревшие помечайте deprecated: true.

Как валидировать контракты в CI?

Относитесь к Schema как к части кода и валидируйте её на каждом коммите.

# Проверить образец данных по Schema (с помощью ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false

В CI запускайте три вещи:

  1. Валидность самой Schema: корректно ли объявление $schema и разрешается ли $ref;
  2. Соответствие фикстур Schema: гарантирует, что тестовые данные не дрейфуют;
  3. Обратная совместимость контракта: сравнить с предыдущей версией и проверить удалённые поля или изменённые типы.

Третий пункт требует поддержки инструментов (например, openapi-diff или json-schema-diff); без него как минимум сравните изменения в массивах required и наборах ключей properties скриптом.

3. Быстрый справочник

Вопрос Рекомендация
Структура ответа {"data":..., "meta":...}, с HTTP-кодами статуса для ошибок
Стиль именования camelCase (фронтенд-первый) или snake_case (данные-первые), согласованно по всему сайту
Формат времени Строки RFC 3339 с часовым поясом, имена полей с суффиксом At
Null Используйте null для отсутствия значения, опускайте неактуальные поля, опускание в PATCH означает «не менять»
Именование булевых Префиксы is/has/can, без отрицаний, без 0/1
Перечисления Строки, строчные буквы с подчёркиваниями, добавить enum в Schema
Формат конфигурации JSON для программной валидации, TOML или JSONC для ручного редактирования
Фикстуры Один базовый вариант + переопределения минимальных различий, валидные по Schema
Конфликты слияния Унифицировать форматирование, упорядочить ключи, разделить файлы
Совместимость версий Добавлять, не менять, не удалять; помечать устаревшим перед удалением