[Из песочницы] JSON Schema и ее использование для валидация JSON-документов в C++
В данной статье описывается стандарт JSON Schema и его использование для проверки соответствия заданному формату на языке C++ средствами библиотеки valijson.Немного истории
Для начала вспомним, что привело к повсеместному вытеснению JSON-ом XML-а и что в этом было плохого. XML изначально создавался как метаязык разметки документов, позволяя использовать унифицированный код парсера и валидатора документов. Будучи первым стандартом такого рода, да еще и пришедшимся на период бурного внедрения цифровых корпоративных информационных систем, XML послужил основой для бесчисленного множества стандартов сериализации данных и протоколов взаимодействия, т.е. хранения и передачи структурированных данных. Тогда как создавался он прежде всего для разметки документов.
Будучи разрабатываемым комитетами, стандарт XML оказался дополнен множеством расширений, позволяющих, в частности, избегать конфликтов имен и выполнять сложные запросы в XML-документах. И, самое важное, поскольку получающееся нагромождение тэгов оказывалось совершенно нечитаемым никаким человеком, был разработан и широко реализован стандарт XML Schema, позволяющий на том же XML абсолютно строго описать допустимое содержимое каждого документа с целью последующей автоматической проверки.
Тем временем, все больше разработчиков под влиянием зарождающихся интерактивных web-технологий стало знакомиться с языком JavaScript, и они начали осознавать, что для представления структурированных объектов в текстовом виде совершенно не обязательно изучать много сотен страниц XML-спецификаций. И когда Дуглас Крокфорд предложил стандартизовать подмножество JavaScript для сериализации объектов (но не разметки документов!) безотносительно к языку, идея была поддержана сообществом. В настоящее время JSON является одним из двух (вместе с XML) языков, поддерживаемых всеми сколько-либо популярными технологиями программирования. Тот же YAML, призванный сделать JSON более удобным и человекочитаемым, ввиду своей сложности (т.е. широты возможностей) распространен не так широко (в моей компании не так давно были проблемы с работой с YAML из MATLAB, тогда как с JSON все хорошо).
Так вот, массово начав использовать JSON для представления данных, разработчики столкнулись с необходимостью вручную проверять содержимое документов, каждый раз на каждом языке переизобретая логику валидации. Людей, знакомых с XML Schema, это не могло не бесить. И постепенно аналогичный стандарт JSON Schema таки сформировался и живет по адресу http://json-schema.org/.
JSON Schema
Рассмотрим пример простой, но показательной, схемы, задающей словарь 2D или 3D геометрических точек в пространстве (-1, 1)x (-1, 1)x (-1, 1) с ключами, состоящими из цифр:
{
"type": "object",
"patternProperties": {
"^[0-9]+$": {
"type": "object",
"properties": {
"value": {
"type": "number",
"minimum": 0
}
"x": { "$ref": "#/definitions/point_coord" },
"y": { "$ref": "#/definitions/point_coord" },
"z": { "$ref": "#/definitions/point_coord" }
},
"required": ["value", "x", "y"]
}
}
"additionalProperties": false,
"definitions": {
"point_coord": {
"type": "number",
"minimum": -1,
"maximum": 1
}
}
}
Если простить Крокфорду надоедливые кавычки, из данного докуменда должно быть ясно, что мы согласны иметь дело с объектом (словарем), ключи которого должны состоять из цифр (см регулярное выражение), значения которого обязаны иметь поля x, y, value, и могут иметь поле z, причем value — неотрицательное число, а x, y, z все имеют некий одинаковый тип point_coord, соответствующий числу от -1 до +1. Даже если предположить, что других возможностей JSON Schema не предоставляет (что далеко от истины), этого должно хватить для многих сценариев использования.
Но это в том случае, если для вашего языка/платформы реализован валидатор. В случае с XML такой вопрос вряд ли мог бы встать.
На http://json-schema.org/ сайте вы можете найти список ПО для валидации. И вот в этом месте незрелость JSON-Schema (и ее сайта) дает о себе знать. Для C++ указана одна (вроде бы интересная) библиотека libvariant, которая занимается валидацией лишь по совместительству и к тому же выпущена под зловредной лицензией LGPL (прощай, iOS). Для C у нас тоже один вариант, и тоже под LGPL.
Тем не менее, приемлемое решение существует и называется valijson. У этой библиотеки есть все что нам нужно (валидация схем и BSD-лицензия), и даже больше, — независимость от JSON-парсера. Valijson позволяет использовать любой json-парсер посредством адаптера (в комплекте адаптеры для jsoncpp, json11, rapidjson, picojson и boost: property_tree), таким образом не требуя переходить на новую json-библиотеку (или тащить за собой еще одну). Плюс ко всему, она состоит только из заголовочных файлов (header only) и не требует компиляции. Очевидный минус только один, и то не для всех, — зависимость от boost. Хотя есть надежда на избавление даже от этого недо-недостатка.
Разберем на примере документа составление JSON-схемы и валидацию этого документа.
Пример составления схемы
Допустим, у нас есть таблица неких полосатых объектов, для которых задана конкретная полосатая раскраска (в виде последовательности 0 и 1, соответствующих черному и белому).
{
"0inv": {
"width": 0.11,
"stripe_length": 0.15,
"code": "101101101110"
},
"0": {
"width": 0.05,
"stripe_length": 0.11,
"code": "010010010001"
},
"3": {
"width": 0.05,
"stripe_length": 0.11,
"code": "010010110001"
},
...
}
Здесь мы имеем словарь с числовыми ключами, к которым может быть приписан суффикс «inv» (для инвертированных штрих-кодов). Все значения в словаре являются объектами и обязаны иметь поля «width», «stripe_length» (строго положительные числа) и «code» (строка нулей и единиц длины 12).
Начнем составлять схему, указав ограничения на формат имен полей верхнего уровня:
{
"comment": "Schema for the striped object specification file",
"type": "object",
"patternProperties": {
"^[0-9]+(inv)?$": { }
},
"additionalProperties": false
}
Здесь мы воспользовались конструктом patternProperties, разрешающим/специфицирующим значения, ключи которых удовлетворяют регулярному выражению. Также мы указали (additionalProperties=false), что неспецифицированные ключи запрещены. Используя additionalProperties, можно не только разрешить или запретить неуказанные явно поля, но и наложить ограничения на их значения, указав в качестве значения спецификатор типа, например, так:
{
"additionalProperties": {
"type": "string",
"pattern": "^Comment: .*$"
}
}
Далее опишем тип значения каждого объекта в словаре:
{
"type": "object",
"properties": {
"width": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"stripe_length": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"code": {
"type": "string",
"pattern": "^[01]{12}$"
}
},
"required": ["width", "stripe_length", "code"]
}
Здесь мы явно перечисляем разрешенные поля (properties), требуя их наличие (required), не запрещая (по умолчанию) любые дополнительные свойства. Числовые свойства у нас строго положительные, а строка code должна соответствовать регулярному выражению.
В принципе осталось только вставить описание типа отдельного объекта в вышеописанную схему таблицы. Но прежде чем это сделать, отметим, что у нас дублируется спецификация полей «width» и «stripe_length». В реальном коде, из которого взят пример, таких полей еще больше, поэтому полезно было бы один раз определить данный тип, а потомы ссылаться на него отосвюду. Именно для этого есть механизм ссылок ($ref). Обратите внимание на секцию definitions в итоговой схеме:
{
"comment": "Schema for the striped object specification file",
"type": "object",
"patternProperties": {
"^[0-9]+(inv)?$": {
"type": "object",
"properties": {
"width": { "$ref": "#/definitions/positive_number" },
"stripe_length": { "$ref": "#/definitions/positive_number" },
"code": {
"type": "string",
"pattern": "^[01]{12}$"
}
},
"required": ["width", "stripe_length", "code"]
}
},
"additionalProperties": false,
"definitions": {
"positive_number": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
}
}
}
Сохраним ее в файл и приступим к написанию валидатора.Применение valijson
В качестве json-парсера используем jsoncpp. Имеем обычную функцию загрузки json-документа из файла:
#include
Json::Value load_document(std::string const& filename)
{
Json::Value root;
Json::Reader reader;
std::ifstream ifs(filename, std::ifstream::binary);
if (!reader.parse(ifs, root, false))
throw std::runtime_error("Unable to parse " + filename + ": "
+ reader.getFormatedErrorMessages());
return root;
}
Минимальная функция-валидатор, сообщающая нам о расположении всех ошибок валидации, выглядит примерно так:
#include
#include
#include
#include
#include
void validate_json(Json::Value const& root, Json::Value const& schema_js)
{
using valijson::Schema;
using valijson::SchemaParser;
using valijson::Validator;
using valijson::ValidationResults;
using valijson::adapters::JsonCppAdapter;
JsonCppAdapter doc(root);
JsonCppAdapter schema_doc(schema_js);
SchemaParser parser(SchemaParser::kDraft4);
Schema schema;
parser.populateSchema(schema_doc, schema);
Validator validator(schema);
validator.setStrict(false);
ValidationResults results;
if (!validator.validate(doc, &results))
{
std::stringstream err_oss;
err_oss << "Validation failed." << std::endl;
ValidationResults::Error error;
int error_num = 1;
while (results.popError(error))
{
std::string context;
std::vector::iterator itr = error.context.begin();
for (; itr != error.context.end(); itr++)
context += *itr;
err_oss << "Error #" << error_num << std::endl
<< " context: " << context << std::endl
<< " desc: " << error.description << std::endl;
++error_num;
}
throw std::runtime_error(err_oss.str());
}
}
Обратим внимание, что в данном примере jsoncpp подключается как #include
, тогда как valijson/adapters/jsoncpp_adapter.hpp
в текущей версии valijson предполагает, что jsoncpp подключается как #include
. Так что не удивляйтесь, если компилятор не найдет json/json.h
, и просто поправьте valijson/adapters/jsoncpp_adapter.hpp
.
Теперь мы можем загружать и валидировать документы:
Json::Value const doc = load_document("/path/to/document.json");
Json::Value const schema = load_document("/path/to/schema.json");
try
{
validate_json(doc, schema);
...
return 0;
}
catch (std::exception const& e)
{
std::cerr << "Exception: " << e.what() << std::endl;
return 1;
}
Все, мы научились валидировать json-документы. Но обратим внимание, что теперь нам придется думать, где хранить схемы! Ведь если документ каждый раз меняется и получается, например, из web-запроса или из аргумента командной строки, то схема неизменна и должна поставляться вместе с приложением. А для небольших программ без развитого механизма загрузки статических ресурсов необходимость введения такового представляет значительный барьер для внедрения валидачии через схемы. Вот было бы здорово компилировать схему вместе с программой, ведь изменение схемы в любом случае потребует изменения кода, обрабатывающего документ.
Это возможно и даже довольно удобно, если в нашем распоряжении есть C++11. Решение примитивное, но работает прекрасно: мы просто определяем строковую константу с нашей схемой. А чтоб не заботиться о кавычках внутри строки, мы используем raw string literal:
// Схема как R"(raw string)"
static std::string const MY_SCHEMA =
R"({
"comment": "Schema for pole json specification",
"type": "object",
"patternProperties": {
"^[0-9]+(inv)?$": {
...
...
}
}
...
})";
// Загрузка json из строки
Json::Value json_from_string(std::string const& str);
{
Json::Reader reader;
std::stringstream schema_stream(str);
Json::Value doc;
if (!reader.parse(schema_stream, doc, false))
throw std::runtime_error("Unable to parse the embedded schema: "
+ reader.getFormatedErrorMessages());
return doc;
}
// Собственно валидация документа doc (validate_json определена выше)
validate_json(doc, json_from_string(MY_SCHEMA));
Т.о. мы имеем удобный кроссплатформенный кросс-языковой механизм валидации json-документов, использование которого в C++ не требует ни линковки внешних библиотек с неудобными лицензиями, ни возни с путями к статическим ресурсам. Эта вещь может сэкономить действительно много сил, и, что немаловажно, помочь окончательно убить XML как формат представления объектов, ибо он неудобен ни для людей, ни для машин.