Контракт JSON API
Согласование формата позволяет клиенту получить JSON, но JSON сам по себе не объясняет смысл полей. Два объекта могут иметь правильный синтаксис и обозначать разные вещи. Контракт API задаёт имена, типы, обязательность и ограничения; это соглашение о данных поверх общей формы JSON.
В этом уроке закрепим договор чтения и изменения каталога. Он не является спецификацией отдельного стандарта с похожим названием JSON:API. Мы говорим об обычном API с представлением application/json. Используем те же курсы и сохраняем простой предметный результат: клиент умеет показать курс и подготовить разрешённый документ редактирования, не угадывая его устройство.
Представление чтения
Вот полный объект курса JavaScript в начальном снимке. В нём нет необъяснённых служебных полей:
{"id":"c001","title":"Современный JavaScript","topic":"frontend","lessons":40}
id — непустая строка, назначенная сервером; клиент хранит её как непрозрачный идентификатор. title — название для человека. topic — машинное значение из небольшого перечня направлений. lessons — целое число уроков. Все четыре поля присутствуют в успешном единичном ответе. Их порядок не имеет прикладного значения: программа читает по именам, а не по расположению.
JSON различает строки, числа, логические значения, массивы, объекты и null. Строка "40" не является тем же входным значением, что число 40. Если библиотека языка умеет неявно преобразовать одно в другое, это ещё не правило API. Наш договор требует число и не исправляет строку автоматически, иначе ошибки клиента окажутся скрыты за случайными преобразованиями.
У JSON нет отдельного типа даты. Если позже появится момент изменения, потребуется договор о строке, часовом поясе и точности. Не следует обещать «дату JSON» без описания. Аналогично большие числовые идентификаторы могут терять точность у некоторых клиентов; наш строковый id избегает зависимости идентичности от числового диапазона языка. Форма данных JSON.
Разрешённый вход
Документ создания и полной замены содержит только редактируемые поля. Полный пример нового курса:
{"title":"HTTP и API","topic":"backend","lessons":32}
title после удаления крайних пробелов должен иметь от одного до 120 кодовых точек Unicode. Это наше простое учебное определение длины; оно не равно количеству байтов UTF-8 и не всегда равно количеству видимых знаков. Сложные эмодзи способны состоять из нескольких кодовых точек. Если продукту нужна длина в воспринимаемых символах, договор должен изменить именно правило, а не только одну функцию клиента.
topic принимает frontend, quality, tools или backend. Эти строки регистрозависимы по нашему соглашению. Название направления в интерфейсе может быть переведено, но машинное значение не меняется вместе с языком кнопки. lessons принимает целое число от одного до тысячи; ноль не обозначает отсутствие данных.
Все три поля обязательны и для POST, и для PUT. Поле id во входе отвергается, а неизвестный ключ также приводит к отказу. Это сознательное различие: сервер может расширять ответы, но не принимает произвольные изменения от клиента. Нельзя получить запись, дополнить её свойством isPublished и считать, что сервис обязан применить его.
Для документации можно записать точную таблицу входа:
| Поле | Тип | Правило |
|---|---|---|
| title | string | После удаления крайних пробелов 1–120 кодовых точек |
| topic | string | Одно из четырёх указанных значений |
| lessons | number | Целое, от 1 до 1000 |
Таблица не заменяет семантику операции: эти поля целиком задают редактируемое представление. Если клиент хочет изменить только название, он сначала получает остальные текущие значения и отправляет согласованный PUT, а позже добавит условие версии. Отдельный PATCH требует собственного формата и здесь ещё не поддерживается.
Пропуск и null
Рассмотрим три входных варианта количества уроков: отсутствующее поле, явный null и ноль. Они имеют различную форму, но все неприемлемы в текущем договоре. Отсутствие нарушает обязательность; null имеет неподходящий тип; ноль выходит за диапазон.
{"title":"HTTP и API","topic":"backend","lessons":null}
Этот объект синтаксически корректен. Сервер не может трактовать null как команду «оставить прежнее значение», потому что такого правила мы не вводили. Для PUT подобная догадка превратила бы полную замену в скрытое частичное изменение. Клиент должен получить структурированную ошибку с указанием поля, а прежняя запись останется без изменений.
Если в будущем появится необязательное описание, нужно будет отдельно выбрать: пропуск означает отсутствие свойства, null — явно неизвестное значение или команду очистить? Эти варианты не стоит смешивать. Ввод нового поля может потребовать разных правил чтения, создания и изменения, особенно когда старые клиенты его ещё не знают.
Повторяющиеся имена объекта создают ещё одну опасность. Например, две записи lessons способны читаться разными парсерами с разным исходом. Наш договор отвергает такие документы ещё при разборе. Это требование к выбранной реализации парсинга: обычное преобразование в словарь уже может потерять сведения о повторах. До запуска сервера следует убедиться, что библиотека позволяет обнаружить этот случай.
Значение и запись числа
Две JSON-записи способны иметь разное текстовое оформление и одно разобранное числовое значение. Например, запись с десятичной точкой, представляющая математически целое число, требует осознанной политики реализации. Наш договор проверяет целочисленное значение, а не наличие точки в исходной строке. Это отличается от текстового параметра limit, у которого отдельно определён допустимый синтаксис.
Для диапазона lessons от одного до тысячи проблем точности обычных числовых клиентов нет. Если позже появится сумма денег или очень большой счётчик, переносить это правило автоматически нельзя. Денежное представление требует единицы и точности, а большой целый идентификатор может быть лучше строкой. Название JSON number само по себе не решает прикладную арифметику.
При этом пробелы и порядок свойств не должны определять предметный смысл объекта. Стабильная сериализация важна для сильного валидатора, но чтение полей клиентом не должно зависеть от неё. Контракт данных и контракт байтового представления связаны, однако отвечают на разные вопросы. Именно это различие пригодится при сохранении отпечатка повторяемой операции.
Расширение без неожиданной поломки
Пусть сервер позже добавит в ответ поле summary. Старый клиент, читающий известные обязательные свойства, должен продолжить показывать курс. Поэтому потребителю ответа полезно допускать новые неизвестные поля, сохраняя проверки тех, от которых зависит его работа. Это не означает доверять любому значению известного поля или безусловно выводить все ключи в интерфейс.
Напротив, переименование lessons в count удаляет обещанное поле и ломает старого потребителя. Замена числа строкой тоже меняет контракт, даже если человеку на экране видны те же цифры. Совместимость оценивают по поведению клиента, а не только по внешнему сходству JSON. Явный договор помогает увидеть такие изменения до публикации.
Добавление нового значения topic может повлиять на клиента со строгим перечнем. Для ответа ему стоит предусмотреть понятное неизвестное направление; для входа редактора сервер всё равно контролирует разрешённые значения. Правила перечислений требуют той же осторожности, что имена полей: расширение словаря не всегда безвредно для программы с исчерпывающим переключением.
Сохраните теперь два документа: представление чтения с id и разрешённый вход без id. Объясните, почему их нельзя автоматически считать одной моделью формы. Затем измените тип lessons и укажите место отказа до сохранения. Следующий урок перенесёт эту точность в адрес: путь выберет идентичность, параметры — настройки выборки, не смешивая их с JSON тела.