Многие сервисы и приложения (особенно веб-сервисы) принимают древовидные данные. Например, такую форму имеют данные, поступающие через JSON-PRC, JSON-REST, PHP-GET/POST. Естественно, появляется задача валидировать их структуру. Существует много вариантов решения этой задачи, начиная от нагромождения if-ов в контроллерах и заканчивая классами, реализующими валидацию по разнообразным конфигурациям. Чаще всего для решения этой задачи требуется рекурсивный валидатор, работающий со схемами данных, описанными по определённому стандарту. Одним из таких стандартов является JSON-Schema, рассмотрим его поближе.
JSON-schema — это стандарт описания структур данных в формате JSON, разрабатываемый на основе XML-Schema, драфт можно найти здесь (далее описанное будет соответствовать версии 03). Схемы, описанные этим стандартом, имеют MIME «application/schema+json». Стандарт удобен для использования при валидации и документировании структур данных, состоящих из чисел, строк, массивов и структур типа ключ-значение (которые, в зависимости от языка программирования, могут называться: объект, словарь, хэш-таблица, ассоциативный массив или карта, далее будет использоваться название «объект» или «object»). На данный момент имеются полные и частичные реализации для разных платформ и языков, в частности javascript, php, ruby, python, java.
Схема
Схема является JSON-объектом, предназначенным для описания каких-либо данных в формате JSON. Свойства этого объекта не являются обязательными, каждое их них является инструкцией определённого правила валидации (далее — правило). Прежде всего, схема может ограничивать тип данных (правило type или disallow, может быть как строкой, так и массивом):
- string (строка)
- number (число, включая все действительные числа)
- integer (целое число, является подмножеством number)
- boolean (true или false)
- object (объект, в некоторых языках зовётся ассоциативным массивом, хэшем, хэш-таблицей, картой или словарём)
- array (массив)
- null («нет данных» или «не известно», возможно только значение null)
- any (любой тип, включая null)
Далее, в зависимости от типа проверяемых данных, применяются дополнительные правила. Например, если проверяемые данные являются числом, к нему могут быть применены minimum, maximum, divisibleBy. Если проверяемые данные являются массивом, в силу вступают правила: minItems, maxItems, uniqueItems, items. Если проверяемые данные являются строкой, применяюся: pattern, minLength, maxLength. Если же проверяется объект, рассматриваются правила: properties, patternProperties, additionalProperties.
Помимо специфичных для типа правил, есть дополнительные обобщённые правила, такие как required и format, а так же описательные правила, такие как id, title, description, $schema. Спецификация определяет несколько микроформатов, таких как: date-time (ISO 8601), date, time, utc-millisec, regex, color (W3C.CR-CSS21-20070719), style (W3C.CR-CSS21-20070719), phone, uri, email, ip-address (V4), ipv6, host-name, которые могут дополнительно проверяться, если определены и поддерживаются текущей реализацией. Более детально с этими и другими правилами можно ознакомиться в спецификации.
Поскольку схема является JSON-объектом, она тоже может быть проверена соответствующей схемой. Схема, которой соответствует текущая схема, записывается в атрибуте $schema. По нему можно определить версию драфта, который был использован для написания схемы. Найти эти схемы можно здесь.
Одной из самых мощных и привлекательных функций JSON-Schema является возможность из схемы ссылаться на другие схемы, а так же наследовать (расширять) схемы (с помощью ссылок JSON-Ref). Делается это с помощью id, extends и $ref. При расширении схемы нельзя переопределять правила, только дополнять их. При работе валидатора к проверяемым данным должны применяться все правила из родительской и дочерней схемы. Рассмотрим далее на примерах.
Примеры
Допустим, есть информация о товарах. У каждого товара есть имя. Это строка от 3 до 50 символов, без пробелов на концах. Определим схему для имени товара:
{
"$schema": "http://json-schema.org/draft-03/schema#", // ид схемы для этой схемы
"id": "urn:product_name#",
"type": "string",
"pattern": "^\\S.*\\S$",
"minLength": 3,
"maxLength": 50,
}
Отлично, теперь этой схемой можно описывать или валидировать любую строку на соответствие имени товара. Далее, у товара есть неотицательная цена, тип ('phone' или 'notebook'), и поддержка wi-fi n и g. Определим схему для товара:
{
"$schema":"http://json-schema.org/draft-03/schema#",
"id": "urn:product#",
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"extends": {"$ref": "urn:product_name#"},
"required": true
},
"price": {
"type": "integer",
"min": 0,
"required": true
},
"type": {
"type": "string",
"enum": ["phone", "notebook"],
"required": true
},
"wi_fi": {
"type": "array",
"items": {
"type": "string",
"enum": ["n", "g"]
},
"uniqueItems": true
}
}
}
В данной схеме используется ссылка на предыдущую схему и расширение её правилом required. Этого нельзя делать в предыдущей схеме, потому что где-нибудь имя может быть необязательным, а все правила будут применяться.
Производительность
Производительность валидатора на основе JSON-Schema, разумеется, развисит от реализации валидатора и полноты поддержки правил. Сделаем тест на nodejs и наиболее «полного» валидатора JSV (установить можно через «npm install JSV»). Сначала сгенерируем тысячу разных продуктов с невалидными свойствами, затем прогоним их через валидатор. После этого покажем количество ошибок каждого типа.
Исходный код теста
var jsv = require('JSV').JSV.createEnvironment();
console.time('load schemas');
jsv.createSchema(
{
"$schema": "http://json-schema.org/draft-03/schema#",
"id": "urn:product_name#",
"type": "string",
"pattern": "^\\S.*\\S$",
"minLength": 3,
"maxLength": 50,
}
);
jsv.createSchema(
{
"$schema":"http://json-schema.org/draft-03/schema#",
"id": "urn:product#",
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"extends": {"$ref": "urn:product_name#"},
"required": true
},
"price": {
"type": "integer",
"min": 0,
"required": true
},
"type": {
"type": "string",
"enum": ["phone", "notebook"],
"required": true
},
"wi_fi": {
"type": "array",
"items": {
"type": "string",
"enum": ["n", "g"]
},
"uniqueItems": true
}
}
}
);
console.timeEnd('load schemas');
console.time('prepare data');
var i, j;
var product;
var products = [];
var names = [];
for (i = 0; i < 1000; i++) {
product = {
name: 'product ' + i
};
if (Math.random() < 0.05) {
while (product.name.length < 60) {
product.name += 'long';
}
}
names.push(product.name);
if (Math.random() < 0.95) {
product.price = Math.floor(Math.random() * 200 - 2);
}
if (Math.random() < 0.95) {
product.type = ['notebook', 'phone', 'something'][Math.floor(Math.random() * 3)];
}
if (Math.random() < 0.5) {
product.wi_fi = [];
for (j = 0; j < 3; j++) {
if (Math.random() < 0.5) {
product.wi_fi.push(['g', 'n', 'a'][Math.floor(Math.random() * 3)]);
}
}
}
products.push(product);
}
console.timeEnd('prepare data');
var errors;
var results = {};
var schema;
var message;
schema = jsv.findSchema('urn:product_name#');
console.time('names validation');
for (i = 0; i < names.length; i++) {
errors = schema.validate(names[i]).errors;
for (j = 0; j < errors.length; j++) {
message = errors[j].message;
if (!results.hasOwnProperty(message)) {
results[message] = 0;
}
results[message]++;
}
}
console.timeEnd('names validation');
console.dir(results);
results = {};
schema = jsv.findSchema('urn:product#');
console.time('products validation');
for (i = 0; i < products.length; i++) {
errors = schema.validate(products[i]).errors;
for (j = 0; j < errors.length; j++) {
message = errors[j].message;
if (!results.hasOwnProperty(message)) {
results[message] = 0;
}
results[message]++;
}
}
console.timeEnd('products validation');
console.dir(results);
Результаты для 1000 проверок вполне удовлетворительные.
(при этом некоторые библиотеки заявляют на порядок большую скорость).
На моем ноутбуке (MBA, OSX, 1.86 GHz Core2Duo):
names validation: 180ms
products validation: 743ms
Заключение
JSON-Schema — достаточно удобный инструмент для документирования структур данных и конфигурирования автоматических валидаторов внешних данных в приложениях. Выглядит проще и читабельнее, чем XML Schema, при этом занимает меньший текстовый объём. Он не зависит от языка программирования и может найти примерение во многих областях: валидация форм POST-запросов, JSON REST API, проверка пакетов при обмене данными через сокеты, валидация документов в документо-ориентированных БД и т. д. Основным преимуществом использования JSON-Schema является стандартизация и, как следствие, упрощение поддержки и улучшение интеграции ПО.