Webrium
Подписаться

JSON как контракт: где он подводит

Формат, который читается без описания, обычно и передают без описания. Что записать отдельно от кода, чтобы обмен пережил расхождение версий.

Контракт существует, только если записан отдельно от кода обеих сторонполе вне схемы

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

Формат без схемы — это не контракт

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

Пока обе стороны пишет одна команда, разницы не видно: договорённость живёт в головах и в коде, и этого хватает. Разница проявляется в момент, когда сторон становится две, или когда между ними появляется время — версия, выпущенная полгода назад, разговаривает с версией, выпущенной вчера.

Отсюда рабочее определение: контракт существует, только если он записан отдельно от кода обеих сторон. Всё остальное — совпадение привычек, которое держится, пока людей мало.

Тупик: проверять на приёме

Первое, что пробуют, когда приходит неожиданный ответ, — добавить проверки в код приёмной стороны. Поле отсутствует — подставим значение по умолчанию. Тип не тот — приведём. Пришёл лишний ключ — проигнорируем.

Это не помогает по двум причинам. Первая: проверки расползаются. Через полгода обработка ответа занимает больше кода, чем полезная работа с ним, и каждая ветка написана под случай, который однажды видели.

Вторая, более важная: такие проверки скрывают факт расхождения. Отправляющая сторона не узнаёт, что нарушила договорённость, — принимающая молча приспособилась. Ошибка перестаёт быть событием и становится фоном, а через год никто уже не знает, какое поведение правильное.

Второй частый ход — договориться устно и записать в задачу. Работает ровно до того момента, когда задачу закроют и она уйдёт из виду.

Где JSON подводит на ровном месте

Числа. Спецификация не различает целые и дробные и не задаёт точность. Большой идентификатор, отправленный числом, на принимающей стороне может потерять младшие разряды — и это не ошибка обработчика, это допустимое поведение. Идентификаторы поэтому передают строкой, даже когда они выглядят как числа.

Отсутствие и пустота. Поля нет, поле есть со значением «ничего», поле есть с пустой строкой — три разные ситуации, которые регулярно смешивают. «Адрес не указан» и «адрес удалён» — разные факты, и если контракт их не различает, различать их будет случайный код.

Порядок. Порядок ключей в объекте не гарантирован, порядок элементов массива гарантирован. Код, полагающийся на первое, работает годами и ломается при смене библиотеки на отправляющей стороне.

Даты. В формате нет типа для времени. Каждый передаёт как умеет: строкой в одном из форматов, числом секунд, числом миллисекунд. Смешение этих способов внутри одного обмена — обычное дело, и обнаруживается оно смещением на десятилетия.

Кодировка и экранирование. Обычно работает само, но текст с символами вне основного набора умеет ломать наивные обработчики на обеих сторонах.

Дробные значения денег. Двоичное представление дробей не хранит десятичные суммы точно, и сложение цен через такой тип даёт копеечные расхождения, которые всплывают в сверке, а не в разработке. Деньги передают либо строкой, либо целым числом наименьших единиц, и это решение принимают один раз на весь обмен, а не для каждого поля отдельно.

Что делать вместо проверок в коде

Записать схему отдельным файлом. Не в комментарии и не в описании задачи, а рядом с кодом, в виде, который умеет читать программа. Тогда договорённость перестаёт быть устной и её можно проверить машинально.

Проверять на обеих сторонах. Отправляющая — перед отправкой, принимающая — на приёме. Это кажется избыточным, но ловит разные ошибки: отправляющая проверка находит ошибку у автора изменения, принимающая — у автора вызова.

Падать громко, а не приспосабливаться. Ответ, нарушивший схему, должен приводить к явной ошибке с указанием, какое поле и чем не устроило. Молчаливое исправление откладывает разбирательство на неопределённый срок и обычно перекладывает его на другого человека.

Различать необязательное и отсутствующее осознанно. Для каждого поля отвечать на вопрос: что означает его отсутствие. Если ответа нет, поле должно быть обязательным.

Не расширять смысл существующих полей. Соблазн передать новую сущность в старом поле, «пока не сделаем нормально», приводит к тому, что поле начинает означать две вещи, и обе стороны понимают его по-разному.

Держать примеры рядом со схемой. Схема описывает форму, пример показывает смысл. Пара реальных ответов — обычный и граничный — снимает больше вопросов, чем страница описаний, и заодно служит проверкой: если пример перестал проходить схему, разошлось что-то одно, и это видно сразу.

Записывать, кто потребители. Контракт без списка тех, кто им пользуется, нельзя менять осознанно: непонятно, кого сломает удаление поля. Список из трёх строк рядом со схемой превращает опасную правку в проверяемую.

Как менять контракт, не ломая

Правило простое и почти всегда достаточное: добавлять можно, убирать и переименовывать — нельзя.

Новое необязательное поле не ломает старых потребителей, потому что они его не читают. Удалённое поле ломает всех, кто на него рассчитывал, а узнают они об этом в момент выкатки. Переименование — это удаление и добавление одновременно, то есть худший случай.

Если поле всё же надо убрать, порядок обратный привычному: сначала перестают читать, потом перестают писать. Между этими шагами проходит столько времени, сколько живёт самый старый потребитель, и знать это время — отдельная задача, которую стоит решить до, а не после.

Отдельный случай — изменение смысла поля при сохранении имени и типа. Формально ничего не сломалось: схема прежняя, проверки проходят. Фактически сломалось всё, что зависело от прежнего смысла, и ни одна автоматическая проверка этого не заметит. Такое изменение по последствиям равно удалению поля и требует такого же обращения — нового имени, а не тихой подмены.

Позиция редакции

Мы считаем, что схема обмена должна существовать как файл и проверяться автоматически на обеих сторонах, а нарушение — приводить к явной ошибке. Практический вывод: если у вас нет файла, описывающего ответ, у вас нет контракта — есть привычка, и она сломается при первом же расхождении версий.

Мы неправы, если обе стороны обмена выпускаются одновременно одной командой и никогда не расходятся по версиям: для внутреннего вызова внутри одного приложения формальная схема — лишний слой, который переживёт свою пользу.

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

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

Контракты данных: как перестать чинить отчёты

Тот же приём с другой стороны: контракт закрывает не один сломанный отчёт, а причину, по которой он ломается каждый раз.

Поле, разделённое швом: слева данные как есть, справа — как их ожидают; элементы пересекают границу, показывая расхождение между реальной схемой и ожиданиямиданные как естькак их ожидают
Практика

Схема поехала: пять способов узнать об этом первым

Пять автоматических проверок на пути данных — от источника до потребителя, — чтобы о поехавшей схеме узнавать раньше, чем сломается отчёт.

Одна строка слева даёт столько строк результата, сколько нашлось пар справастрок на входе
Углубление

Соединения таблиц: почему строк больше, чем ожидали

Глубже про сами соединения: число строк задаёт кратность связи, и одну проверку можно сделать до запуска запроса.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть