Немного истории
Для начала вспомним, что привело к повсеместному вытеснению 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-cpp/json.h> 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 <valijson/adapters/jsoncpp_adapter.hpp> #include <valijson/schema.hpp> #include <valijson/schema_parser.hpp> #include <valijson/validation_results.hpp> #include <valijson/validator.hpp> 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<std::string>::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 <json-cpp/json.h>, тогда как valijson/adapters/jsoncpp_adapter.hpp в текущей версии valijson предполагает, что jsoncpp подключается как #include <json/json.h>. Так что не удивляйтесь, если компилятор не найдет 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 как формат представления объектов, ибо он неудобен ни для людей, ни для машин.

