Pull to refresh

Описание и валидация древовидных структур данных. JSON-Schema

Reading time 6 min
Views 80K

Многие сервисы и приложения (особенно веб-сервисы) принимают древовидные данные. Например, такую форму имеют данные, поступающие через 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 является стандартизация и, как следствие, упрощение поддержки и улучшение интеграции ПО.
Tags:
Hubs:
+32
Comments 18
Comments Comments 18

Articles