Справочный API 2ГИС разрабатывается уже 4 года. Появилось около 40 методов, которые возвращают достаточно крупные и иерархически сложные структуры в формате JSON и XML. Совсем недавно я решил поделиться накопленным опытом и выступить на конференции DevConf.
Одна из тем доклада вызвала наибольший интерес у участников — это использование JSON-Schema при тестировании формата выдачи API. В этой статье я расскажу, какие задачи решает этот подход, какие имеет ограничения, что вы получаете из коробки, а что идёт бонусом. Поехали!

JSON-Schema — это JSON-аналог формата XML Schema. Суть его в том, что он в декларативном виде задает структуру документа. Ничто не мешает использовать JSON-схемы для целей тестирования.
Казалось бы, какие могут быть сложности в тестировании API? Это же не какой-нибудь изощренный UI. Подумаешь, выполняем запрос и сравниваем результат с ожидаемым.
Расстраивают громоздкие иерархические структуры, которые могут быть в запросах или ответах. Когда параметров в районе 50-ти, учесть все возможные характерные случаи крайне проблематично.
В дополнение к тому, что API должен выполнять возложенные на него обязанности, ждешь от него продуманности и логичности. В том числе это относится к формату. Хочется, чтобы числа всегда отдавались, как числа, а не строки. Что если массив пустой, он не должен быть равен null, или вообще отсутствовать в ответе, а пусть он будет пустым массивом. Это, конечно, мелочи, но пользователям API очень неприятно о них запинаться. Плюс это может приводить к скрытым ошибкам. Так что строгости формата следует уделять внимание.
В целом тестирование структуры и формата — задача не хитрая. Для каждого метода можно описать структуру запроса и ответа с помощью формата JSON-Schema.
Как говорится, все уже придумано до нас. Следует просто грамотно этим воспользоваться.
Мы будем рассматривать тестирование формата именно ответов API. Просто это нам ближе, т.к. наше API ориентировано на только чтение данных. Но когда API умеет принимать данные в виде сложных объектов, то принципиальный подход остается таким же, как и в случае чтения.
Формат
Значит, поможет нам в нашем нелегком деле JSON-Schema. Краткое введение в сам формат уже есть на хабре. Поэтому ограничимся тривиальным примером. Возьмем JSON-объект:
Чтобы задать его структуру, тип параметров, обязятельность, достаточно заменить значения параметров на специальные объекты. Смотрим:
Согласитесь, вполне наглядно. По большому счету, JSON-Schema стремится быть аналогом формату XML Schema.
PHP-реализация
Конечно, не обязательно при описании ответа использовать именно JSON-Schema. Формат относительно молодой, не до конца устаканился. Не так много библиотек и инструментов, как для формата XML Schema. Но для очень многих разработчиков формат JSON гораздо нагляднее. А если говорить про PHP, то JSON почти родной для него благодаря функциям json_encode и json_decode.
Реализации формата JSON-Schema есть для разных языков. Как видно, выбор не велик. Для PHP две библиотеки от двух институтов: MIT и Berkeley.
На момент написания статьи последний комит в MIT — февраль 2012, а в Berkeley — июнь 2013. Так что, беглый обзор подтолкнул к выбору именно Berkeley.
В качестве ложки дегтя можно отметить, что для объектов в ответе могут присутствовать не только описанные в схеме поля, но и совершенно левые. Причем ответ будет совершенно валидным, что в большинстве случаев не допустимо. Лечится это, на мой взгляд, недоразумение маленьким препроцессором, который явно устанавливает специальное свойство additionalProperties в false по умолчанию.
Включать валидацию ответов API на бою особого смысла нет, лишние накладные расходы. А вот когда мы прогоняем тесты — это самое оно. Для переключения режимов достаточно флага в конфигурационном файле приложения.
Если говорить про конкретную библиотеку Berkeley, то пример валидации из тестов:
Даже интуитивно понятно, что происходит.
Следует заметить, что писать тесты специально для проверки JSON-схемы нет необходимости. Считаем, что на API уже написаны функциональные тесты. Это значит, что если мы включим фильтрацию, то при выполнении тестов автоматически в фоновом режиме будет проверятся формат ответа. Как бы между делом. Никаких специальных усилий кроме, собственно, написания схем прикладывать не нужно.
Да, и небольшое дополнение: для тестов производительности валидацию следует отключить, чтобы не было «наводок». А на машинах разработчиков — наоборот, включить, чтобы быстрее выявлять нарушение формата. Вопрос — кому писать JSON-схемы? У нас пишут разработчики, так удобнее. Схемы живут в коде проекта, и при любых изменениях формата разработчик сразу же корректирует схему.
С тестированием разобрались, подключаем к юнит-тестам и на выходе получаем уверенность в правильности формата. Но есть ещё один большой бонус при использовании JSON-схемы, к которому мы пришли — это документирование АПИ.
Проблема с документацией одна — ее, блин, надо писать. Документация к API — это ее спецификация, все нюансы должны быть отражены в ней. Ну а как иначе? API же не существует само по себе, он существует только для того, чтобы его клиенты могли с ним работать. И чем меньше сюрпризов от него ждешь, тем он понятнее и пользователи быстрее и комфортнее могут им пользоваться. Может быть, единственный случай, когда на документации можно сэкономить, если разработчики API — одновременно его пользователи. А это бывает далеко не всегда. Поэтому, лучше бы документация была. Ну а если сумарное количество параметров — сотни, как поддерживать документацию актуальной? Мало того, что пишем код с тестами, так еще и приходится за документацией следить. Короче, плюс один «головняк», коих и без документации хватает.
Так, а что насчет версионирования документации? Дело в том, что у нас, как и у многих, принят подход — каждая фича делается в отдельной ветке в git.
Хорошо, если бы каждой ветке соответствовала своя версия документации. Так всем проще: и разработчику, и тестировщику. Сделал задачу в ветке, тут же написал документацию и «забыл» про задачу.
Поэтому хорошо, когда документация живет вместе с кодом. А чтобы еще и было удобно мержить одного и того же документа из разных задач, логично хранить ее в неком текстовом формате. Можно хранить ее в формате Markdown, семантических bb-кодов или еще как-нибудь. Но по факту это все равно остается почти голый текст, с различными таблицами, связями между ними, перекрестными ссылками и т.п. Не совсем понятно, как тестировать корректность документации. Каждый раз внимательно все проверять вручную, “задалбливаешься”. Это во-первых, а во-вторых, ошибки все равно остаются.
Ну, а все-таки:

Отказаться от нее полностью нельзя, но можно сильно сократить количество усилий для ее поддержки. У нас уже есть JSON-Schema. В ней уже прописаны все названия параметров в ответе API, иерархические связи между ними, типы данных, обязательность параметра. А именно эта информация также нужна для генерации документации.
Поэтому мы решили скрестить бульдога с носорогом и таки писать документацию прямо в JSON-Schema. В формате JSON-Schema уже предусмотрен тег description. Но он должен быть строкой, согласно спецификации. А было бы неплохо еще и примеры добавить (вложенный тег examples), и опции какие-нибудь специфические, типа секретный параметр (вложенные тег hide) и прочее. Поэтому тег-объект лучше для этого подойдет. Мы выбрали название тега — «meta». Пример:
Теперь натравливаем нашу JSON-схему на спец. парсер, и она превращается в элегантную документацию. При формировании документации учитывается не только содержимое нашего собственного тега «meta», но и информация из нативных тегов схемы.
Конкретный способ представления документации может быть разный. Мы предпочли обычные плоские таблицы с ссылками между ними. Но это, конечно, не идеальный вариант. Как бы то ни было, размещение документации в JSON-схеме не привязывает к конечному способу отображения, что дает больше свободы.
Для максимальной гибкости документация не должна строиться полностью из JSON-схемы. У нас хорошо зарекомендовал себя подход, когда для каждой страницы документации есть отдельный текстовый файл. А уже в него вставляется ссылка на конкретную JSON-схему. При сборке страницы JSON-схема преобразуется в финальный текстовый вид. Таким образом, мы решаем проблему размещения в документации произвольного текста, примеров, и прочих материалов. И при этом мы не пытаемся все, что можно, запихнуть в JSON-схему.
Получается, что одни и те же JSON-схемы используются и для тестов и для документации. А это значит, что JSON-схемы всегда актуальны и корректны, иначе тесты свалятся. Так что в документации в принципе не может быть ошибок, связанных с названиями параметров, их типов, обязательности/необязательности, списка допустимых значений, иерархических связей между параметрами. Что, согласитесь, уже не мало.
Документация без примеров использования сильно увеличивает порог вхождения для пользователей API. Поэтому их следует добавлять в обязательном порядке. Мы их организуем следующим образом. Описываем в документации запрос и рядом пример ответа.
Но, как не трудно догадаться, всплывает проблема актуальности текста ответа. Пройдет неделя, формат расширится, и что, примеры заново составлять? Есть выход лучше.
Пришли к тому, что результаты запросов в примерах документации должны быть динамическими. Тут возможны разные подходы. Так как у нас ответы API часто очень крупные, то мы их показываем в документации по клику на определенную область. Именно в этот момент мы и выполняем запрос. Самая простая схема, но с небольшой задержкой получения данных.
Если такой вариант не подходит, можно специальной командой динамически выполнять все запросы в примерах, и заполнять ответы в странице документации. Делать это можно, например, перед релизом.
Кстати, раз уж примеры все равно пишем, то их тоже можно считать разновидностью функциональных тестов. Ну правда, собираем все запросы в документации, включаем JSON-схему и проверяем валидность. Вместе с тем, таким образом можно определить сломанные методы или неправильно составленные запросы.
Говоря о валидации формата, следует определиться, что делать с параметрами с пустыми значениями в ответе. Мы для себя пришли к следующему соглашению.
Параметр в любом случае должны присутствовать в ответе, даже если он содержит пустое значение. Так лучше видна структура ответа. Отпадает необходимость обращаться к документации только для того, чтобы ее узнать.
Параметр типа массив возвращает пустой массив. Объект — null. Для числовых и строковых параметров, если ноль и пустая строка осмысленные значения, то их и возвращаем. Например, параметр «количество отзывов» вполне может возвращать ноль — это логично. А вот параметр «этажность здания» если вернет ноль — это бред. Если, допустим, для определенного дома неизвестна его этажность, следует вернуть null.
Где null возможен, а где нет, явно указывается в JSON-схеме. А значит, и в документации. Ваш подход может отличаться, главное, чтобы он был единообразен для всех параметров, иначе конечным пользователям API добавится головной боли.
JSON-Schema сильно экономит драгоценное время на тестирование и документирование API. Это как раз тот случай, когда небольшое количество усилий приносит много профита. И чем крупнее API, тем больше усилий этот подход позволяет сэкономить.
Правда, один раз придется вложиться в написание небольшого инструментария для использования JSON-схем.
Помимо экономии времени на тесты, проще поддерживать обратную совместимость в API, т.к. формат API наглядно выражается в JSON-схеме. А тяжелый груз документации становится легкой ручной кладью.
Одна из тем доклада вызвала наибольший интерес у участников — это использование JSON-Schema при тестировании формата выдачи API. В этой статье я расскажу, какие задачи решает этот подход, какие имеет ограничения, что вы получаете из коробки, а что идёт бонусом. Поехали!

JSON-Schema — это JSON-аналог формата XML Schema. Суть его в том, что он в декларативном виде задает структуру документа. Ничто не мешает использовать JSON-схемы для целей тестирования.
Казалось бы, какие могут быть сложности в тестировании API? Это же не какой-нибудь изощренный UI. Подумаешь, выполняем запрос и сравниваем результат с ожидаемым.
Расстраивают громоздкие иерархические структуры, которые могут быть в запросах или ответах. Когда параметров в районе 50-ти, учесть все возможные характерные случаи крайне проблематично.
В дополнение к тому, что API должен выполнять возложенные на него обязанности, ждешь от него продуманности и логичности. В том числе это относится к формату. Хочется, чтобы числа всегда отдавались, как числа, а не строки. Что если массив пустой, он не должен быть равен null, или вообще отсутствовать в ответе, а пусть он будет пустым массивом. Это, конечно, мелочи, но пользователям API очень неприятно о них запинаться. Плюс это может приводить к скрытым ошибкам. Так что строгости формата следует уделять внимание.
В целом тестирование структуры и формата — задача не хитрая. Для каждого метода можно описать структуру запроса и ответа с помощью формата JSON-Schema.
Как говорится, все уже придумано до нас. Следует просто грамотно этим воспользоваться.
Тестирование формата API
Запрос и ответ
Мы будем рассматривать тестирование формата именно ответов API. Просто это нам ближе, т.к. наше API ориентировано на только чтение данных. Но когда API умеет принимать данные в виде сложных объектов, то принципиальный подход остается таким же, как и в случае чтения.
JSON-Schema
Формат
Значит, поможет нам в нашем нелегком деле JSON-Schema. Краткое введение в сам формат уже есть на хабре. Поэтому ограничимся тривиальным примером. Возьмем JSON-объект:
{ "a":10, "b":"boo" }
Чтобы задать его структуру, тип параметров, обязятельность, достаточно заменить значения параметров на специальные объекты. Смотрим:
{
"a": {
"type": "number",
"required": true
},
"b": {
"type": "string",
"required": true
}
}
Согласитесь, вполне наглядно. По большому счету, JSON-Schema стремится быть аналогом формату XML Schema.
PHP-реализация
Конечно, не обязательно при описании ответа использовать именно JSON-Schema. Формат относительно молодой, не до конца устаканился. Не так много библиотек и инструментов, как для формата XML Schema. Но для очень многих разработчиков формат JSON гораздо нагляднее. А если говорить про PHP, то JSON почти родной для него благодаря функциям json_encode и json_decode.
Реализации формата JSON-Schema есть для разных языков. Как видно, выбор не велик. Для PHP две библиотеки от двух институтов: MIT и Berkeley.
На момент написания статьи последний комит в MIT — февраль 2012, а в Berkeley — июнь 2013. Так что, беглый обзор подтолкнул к выбору именно Berkeley.
В качестве ложки дегтя можно отметить, что для объектов в ответе могут присутствовать не только описанные в схеме поля, но и совершенно левые. Причем ответ будет совершенно валидным, что в большинстве случаев не допустимо. Лечится это, на мой взгляд, недоразумение маленьким препроцессором, который явно устанавливает специальное свойство additionalProperties в false по умолчанию.
Валидация
Включать валидацию ответов API на бою особого смысла нет, лишние накладные расходы. А вот когда мы прогоняем тесты — это самое оно. Для переключения режимов достаточно флага в конфигурационном файле приложения.
Если говорить про конкретную библиотеку Berkeley, то пример валидации из тестов:
$validator = new Validator();
$validator->check(json_decode($input), json_decode($schema)); // проверяем $input на соответствие схеме $schema
$this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true));
Даже интуитивно понятно, что происходит.
Следует заметить, что писать тесты специально для проверки JSON-схемы нет необходимости. Считаем, что на API уже написаны функциональные тесты. Это значит, что если мы включим фильтрацию, то при выполнении тестов автоматически в фоновом режиме будет проверятся формат ответа. Как бы между делом. Никаких специальных усилий кроме, собственно, написания схем прикладывать не нужно.
Да, и небольшое дополнение: для тестов производительности валидацию следует отключить, чтобы не было «наводок». А на машинах разработчиков — наоборот, включить, чтобы быстрее выявлять нарушение формата. Вопрос — кому писать JSON-схемы? У нас пишут разработчики, так удобнее. Схемы живут в коде проекта, и при любых изменениях формата разработчик сразу же корректирует схему.
Документация
С тестированием разобрались, подключаем к юнит-тестам и на выходе получаем уверенность в правильности формата. Но есть ещё один большой бонус при использовании JSON-схемы, к которому мы пришли — это документирование АПИ.
Проблема с документацией одна — ее, блин, надо писать. Документация к API — это ее спецификация, все нюансы должны быть отражены в ней. Ну а как иначе? API же не существует само по себе, он существует только для того, чтобы его клиенты могли с ним работать. И чем меньше сюрпризов от него ждешь, тем он понятнее и пользователи быстрее и комфортнее могут им пользоваться. Может быть, единственный случай, когда на документации можно сэкономить, если разработчики API — одновременно его пользователи. А это бывает далеко не всегда. Поэтому, лучше бы документация была. Ну а если сумарное количество параметров — сотни, как поддерживать документацию актуальной? Мало того, что пишем код с тестами, так еще и приходится за документацией следить. Короче, плюс один «головняк», коих и без документации хватает.
Версионирование
Так, а что насчет версионирования документации? Дело в том, что у нас, как и у многих, принят подход — каждая фича делается в отдельной ветке в git.
Хорошо, если бы каждой ветке соответствовала своя версия документации. Так всем проще: и разработчику, и тестировщику. Сделал задачу в ветке, тут же написал документацию и «забыл» про задачу.
Поэтому хорошо, когда документация живет вместе с кодом. А чтобы еще и было удобно мержить одного и того же документа из разных задач, логично хранить ее в неком текстовом формате. Можно хранить ее в формате Markdown, семантических bb-кодов или еще как-нибудь. Но по факту это все равно остается почти голый текст, с различными таблицами, связями между ними, перекрестными ссылками и т.п. Не совсем понятно, как тестировать корректность документации. Каждый раз внимательно все проверять вручную, “задалбливаешься”. Это во-первых, а во-вторых, ошибки все равно остаются.
JSON-Schema
Ну, а все-таки:

Отказаться от нее полностью нельзя, но можно сильно сократить количество усилий для ее поддержки. У нас уже есть JSON-Schema. В ней уже прописаны все названия параметров в ответе API, иерархические связи между ними, типы данных, обязательность параметра. А именно эта информация также нужна для генерации документации.
Поэтому мы решили скрестить бульдога с носорогом и таки писать документацию прямо в JSON-Schema. В формате JSON-Schema уже предусмотрен тег description. Но он должен быть строкой, согласно спецификации. А было бы неплохо еще и примеры добавить (вложенный тег examples), и опции какие-нибудь специфические, типа секретный параметр (вложенные тег hide) и прочее. Поэтому тег-объект лучше для этого подойдет. Мы выбрали название тега — «meta». Пример:
"reviews_count": {
"type": "number",
"required": true,
"meta": {
"description": "Количество отзывов",
"hide": true
}
}
Представление
Теперь натравливаем нашу JSON-схему на спец. парсер, и она превращается в элегантную документацию. При формировании документации учитывается не только содержимое нашего собственного тега «meta», но и информация из нативных тегов схемы.
Конкретный способ представления документации может быть разный. Мы предпочли обычные плоские таблицы с ссылками между ними. Но это, конечно, не идеальный вариант. Как бы то ни было, размещение документации в JSON-схеме не привязывает к конечному способу отображения, что дает больше свободы.
Для максимальной гибкости документация не должна строиться полностью из JSON-схемы. У нас хорошо зарекомендовал себя подход, когда для каждой страницы документации есть отдельный текстовый файл. А уже в него вставляется ссылка на конкретную JSON-схему. При сборке страницы JSON-схема преобразуется в финальный текстовый вид. Таким образом, мы решаем проблему размещения в документации произвольного текста, примеров, и прочих материалов. И при этом мы не пытаемся все, что можно, запихнуть в JSON-схему.
Самотестирование
Получается, что одни и те же JSON-схемы используются и для тестов и для документации. А это значит, что JSON-схемы всегда актуальны и корректны, иначе тесты свалятся. Так что в документации в принципе не может быть ошибок, связанных с названиями параметров, их типов, обязательности/необязательности, списка допустимых значений, иерархических связей между параметрами. Что, согласитесь, уже не мало.
Примеры
Документация без примеров использования сильно увеличивает порог вхождения для пользователей API. Поэтому их следует добавлять в обязательном порядке. Мы их организуем следующим образом. Описываем в документации запрос и рядом пример ответа.
Но, как не трудно догадаться, всплывает проблема актуальности текста ответа. Пройдет неделя, формат расширится, и что, примеры заново составлять? Есть выход лучше.
Пришли к тому, что результаты запросов в примерах документации должны быть динамическими. Тут возможны разные подходы. Так как у нас ответы API часто очень крупные, то мы их показываем в документации по клику на определенную область. Именно в этот момент мы и выполняем запрос. Самая простая схема, но с небольшой задержкой получения данных.
Если такой вариант не подходит, можно специальной командой динамически выполнять все запросы в примерах, и заполнять ответы в странице документации. Делать это можно, например, перед релизом.
Кстати, раз уж примеры все равно пишем, то их тоже можно считать разновидностью функциональных тестов. Ну правда, собираем все запросы в документации, включаем JSON-схему и проверяем валидность. Вместе с тем, таким образом можно определить сломанные методы или неправильно составленные запросы.
Пустые значения
Говоря о валидации формата, следует определиться, что делать с параметрами с пустыми значениями в ответе. Мы для себя пришли к следующему соглашению.
Параметр в любом случае должны присутствовать в ответе, даже если он содержит пустое значение. Так лучше видна структура ответа. Отпадает необходимость обращаться к документации только для того, чтобы ее узнать.
Параметр типа массив возвращает пустой массив. Объект — null. Для числовых и строковых параметров, если ноль и пустая строка осмысленные значения, то их и возвращаем. Например, параметр «количество отзывов» вполне может возвращать ноль — это логично. А вот параметр «этажность здания» если вернет ноль — это бред. Если, допустим, для определенного дома неизвестна его этажность, следует вернуть null.
Где null возможен, а где нет, явно указывается в JSON-схеме. А значит, и в документации. Ваш подход может отличаться, главное, чтобы он был единообразен для всех параметров, иначе конечным пользователям API добавится головной боли.
Заключение
JSON-Schema сильно экономит драгоценное время на тестирование и документирование API. Это как раз тот случай, когда небольшое количество усилий приносит много профита. И чем крупнее API, тем больше усилий этот подход позволяет сэкономить.
Правда, один раз придется вложиться в написание небольшого инструментария для использования JSON-схем.
Помимо экономии времени на тесты, проще поддерживать обратную совместимость в API, т.к. формат API наглядно выражается в JSON-схеме. А тяжелый груз документации становится легкой ручной кладью.