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

от автора


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

ссылка на оригинал статьи http://habrahabr.ru/post/158927/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *