Описание и валидация древовидных структур данных. 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 является стандартизация и, как следствие, упрощение поддержки и улучшение интеграции ПО.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 18

      +1
      а на основе relax-ng ещё никто ничего для json не сделал?
      • UFO just landed and posted this here
          0
          Спасибо, взглянул на revalidator, он по всей видимости тоже реализует JSON Schema, но без предзагрузки схем в окружение, наследования и ссылок и с некоторыми расширенными возможностями (такими как нормализация типов). Надо будет потестить. По поводу второго момента я не понял, строгое соответствие и так указано: «additionalProperties»: false.
          • UFO just landed and posted this here
              0
              Этот бенчмарк я смотрел (и даже ссылку на него давал в конце). Revalidator-а там, к сожалению, нет. По поводу скорости да, согласен что JSV можно считать самым тормозным. Но он наиболее полно поддерживает стандарт, и его замену на более быстрый (или вообще специфически самописный), на мой взгляд, можно отнести к задачам оптимизации.
              • UFO just landed and posted this here
          0
          Выглядит проще и читабельнее, чем XML Schema, при этом занимает меньший текстовый объём.
          Это действительно важно? Ведь не обязательно использовать текстовый редактор для работы с xml. С Xml Scheme все равно естественно использовать специализированные инструменты для редактирования, которые предоставляют свой UI.

          И еще вопрос — существуют инструменты для авто генерации схемы по данному на вход json-ну?
            0
            По поводу «проще и читабельнее», я предпочитаю читабельность, из соображений удобства работы с голым текстом, без IDE (например, когда кто-то присылает вопрос с куском схемы на мыло, и тд.).

            Генераторы есть, вот например www.jsonschema.net/
            Но схемы обычно пишутся руками, птому что генератор не может предугадать все правила.
            0
            Можно ли как-то проверять не только соответствие типов атрибутов, но саму структуру Json объекта согласно схеме? Т е осуществлять проверку жесткого соответствия схеме.Например, если в Json объекте появился лишний атрибут, то тест на соответствие созданной ранее схеме завалится? Проверку типов я уже осуществил (java+json-validator+junit), но саму структуру проверить не могу.
              +1
              Конечно, в третьей редакции схемы для типа object у каждого его поля указывается булевый параметр required, а в четвертой — один атрибут required для всего объекта со списком необходимых полей. Наличие дополнительных атрибутов блокируется параметром additionalProperties.
              0
              И как вам опыт использования? Столкнулись ли с какими-то неочевидными проблемами?
              И, если есть такое, можете посоветовать генератор HTML-форм по схеме?
                0
                JSON ведь позволяет использовать объекты как ассоциативные массивы.
                позволяет ли JSON-schema по паттерну проверять ключи?
                  0
                  Картинка взята с сайта json.org
                  Правда позволяет? (картинка взята с json.org)
                    0
                    string — ключ
                    value — значение
                    разве нет?
                      0
                      Прошу прощения. Подумал что под фразой «паттернами проверять ключи» имеется в виду возможность в качестве ключей использовать другие объекты и их проверять.
                    0
                    да, позволяет:
                    patternProperties:{
                      "RegExp":{
                        title:"схема для всех объектов с ключем, удовлетворяющем RegExp"
                      }
                    }
                    
                      0
                      уточнение: схема для всех свойств с ключем, удовлетворяющим RegExp
                    0
                    а позволяет ли JSON hyper-schema проверить строку, что заданный объект документа имеет свойство с именем, равным данной строке?
                    так сказать для проверки документа на битые ссылки

                    Only users with full accounts can post comments. Log in, please.