Как правильно проверить массив объектов с помощью JustinRainbow/JsonSchema

У меня есть код, который правильно проверяет статью, возвращенную из конечной точки, которая возвращает отдельные статьи. Я почти уверен, что он работает правильно, поскольку выдает ошибку проверки, когда я намеренно не включаю обязательное поле в статью.

У меня также есть этот код, который пытается проверить массив статей, возвращенный из конечной точки, которая возвращает массив статей. Однако я почти уверен, что это работает неправильно, так как всегда говорит, что данные действительны, даже когда я намеренно не включаю обязательное поле в статьи.

Как правильно проверить массив данных по схеме?

Полный тестовый код приведен ниже в виде автономного запускаемого теста. Оба теста должны завершиться неудачно, но только один из них.

<?php

declare(strict_types=1);

error_reporting(E_ALL);

require_once __DIR__ . '/vendor/autoload.php';


// Return the definition of the schema, either as an array
// or a PHP object
function getSchema($asArray = false)
{
    $schemaJson = <<< 'JSON'
{
  "swagger": "2.0",
  "info": {
    "termsOfService": "http://swagger.io/terms/",
    "version": "1.0.0",
    "title": "Example api"
  },
  "paths": {
    "/articles": {
      "get": {
        "tags": [
          "article"
        ],
        "summary": "Find all articles",
        "description": "Returns a list of articles",
        "operationId": "getArticleById",
        "produces": [
          "application/json"
        ],
        "responses": {
          "200": {
            "description": "successful operation",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/Article"
              }
            }
          }
        },
        "parameters": [
        ]
      }
    },
    "/articles/{articleId}": {
      "get": {
        "tags": [
          "article"
        ],
        "summary": "Find article by ID",
        "description": "Returns a single article",
        "operationId": "getArticleById",
        "produces": [
          "application/json"
        ],
        "parameters": [
          {
            "name": "articleId",
            "in": "path",
            "description": "ID of article to return",
            "required": true,
            "type": "integer",
            "format": "int64"
          }
        ],
        "responses": {
          "200": {
            "description": "successful operation",
            "schema": {
              "$ref": "#/definitions/Article"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "Article": {
      "type": "object",
      "required": [
        "id",
        "title"
      ],
      "properties": {
        "id": {
          "type": "integer",
          "format": "int64"
        },
        "title": {
          "type": "string",
          "description": "The title for the link of the article"
        }
      }
    }
  },
  "schemes": [
    "http"
  ],
  "host": "example.com",
  "basePath": "/",
  "tags": [],
  "securityDefinitions": {
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ]
}
JSON;

    return json_decode($schemaJson, $asArray);
}

// Extract the schema of the 200 response of an api endpoint.
function getSchemaForPath($path)
{
    $swaggerData = getSchema(true);
    if (isset($swaggerData["paths"][$path]['get']["responses"][200]['schema']) !== true) {
        echo "response not defined";
        exit(-1);
    }

    return $swaggerData["paths"][$path]['get']["responses"][200]['schema'];
}

// JsonSchema needs to know about the ID used for the top-level
// schema apparently.
function aliasSchema($prefix, $schemaForPath)
{
    $aliasedSchema = [];

    foreach ($schemaForPath as $key => $value) {
        if ($key === '$ref') {
            $aliasedSchema[$key] = $prefix . $value;
        }
        else if (is_array($value) === true) {
            $aliasedSchema[$key] = aliasSchema($prefix, $value);
        }
        else {
            $aliasedSchema[$key] = $value;
        }
    }
    return $aliasedSchema;
}


// Test the data matches the schema.
function testDataMatches($endpointData, $schemaForPath)
{
    // Setup the top level schema and get a validator from it.
    $schemaStorage = new \JsonSchema\SchemaStorage();
    $id = 'file://example';
    $swaggerClass = getSchema(false);
    $schemaStorage->addSchema($id, $swaggerClass);
    $factory = new \JsonSchema\Constraints\Factory($schemaStorage);
    $jsonValidator = new \JsonSchema\Validator($factory);

    // Alias the schema for the endpoint, so JsonSchema can work with it.
    $schemaForPath = aliasSchema($id, $schemaForPath);

    // Validate the things
    $jsonValidator->check($endpointData, (object)$schemaForPath);

    // Process the result
    if ($jsonValidator->isValid()) {
        echo "The supplied JSON validates against the schema definition: " . \json_encode($schemaForPath) . " \n";
        return;
    }

    $messages = [];
    $messages[] = "End points does not validate. Violations:\n";
    foreach ($jsonValidator->getErrors() as $error) {
        $messages[] = sprintf("[%s] %s\n", $error['property'], $error['message']);
    }

    $messages[] = "Data: " . \json_encode($endpointData, JSON_PRETTY_PRINT);

    echo implode("\n", $messages);
    echo "\n";
}



// We have two data sets to test. A list of articles.

$articleListJson = <<< JSON
[
  {
      "id": 19874
  },
  {
      "id": 19873
  }
]
JSON;
$articleListData = json_decode($articleListJson);


// A single article
$articleJson = <<< JSON
{
  "id": 19874
}
JSON;
$articleData = json_decode($articleJson);


// This passes, when it shouldn't as none of the articles have a title
testDataMatches($articleListData, getSchemaForPath("/articles"));


// This fails correctly, as it is correct for it to fail to validate, as the article doesn't have a title
testDataMatches($articleData, getSchemaForPath("/articles/{articleId}"));

Минимальный composer.json:

{
    "require": {
        "justinrainbow/json-schema": "^5.2"
    }
}

person Danack    schedule 17.05.2018    source источник


Ответы (4)


Edit-2: 22 мая

Я копал дальше, оказалось, что проблема связана с вашим преобразованием верхнего уровня в object

$jsonValidator->check($endpointData, (object)$schemaForPath);

Вы не должны были просто делать это, и все бы работало

$jsonValidator->check($endpointData, $schemaForPath);

Так что это не похоже на ошибку, это было просто неправильное использование. Если вы просто удалите (object) и запустите код

$ php test.php
End points does not validate. Violations:

[[0].title] The property title is required

[[1].title] The property title is required

Data: [
    {
        "id": 19874
    },
    {
        "id": 19873
    }
]
End points does not validate. Violations:

[title] The property title is required

Data: {
    "id": 19874
}

Редактировать-1

Чтобы исправить исходный код, вам нужно обновить CollectionConstraints.php

/**
 * Validates the items
 *
 * @param array            $value
 * @param \stdClass        $schema
 * @param JsonPointer|null $path
 * @param string           $i
 */
protected function validateItems(&$value, $schema = null, JsonPointer $path = null, $i = null)
{
    if (is_array($schema->items) && array_key_exists('$ref', $schema->items)) {
        $schema->items = $this->factory->getSchemaStorage()->resolveRefSchema((object)$schema->items);
        var_dump($schema->items);
    };

    if (is_object($schema->items)) {

Это точно справится с вашим вариантом использования, но если вы не предпочитаете изменять код из зависимости, используйте мой оригинальный ответ

Исходный ответ

В библиотеке есть ошибка/ограничение, заключающееся в том, что в src/JsonSchema/Constraints/CollectionConstraint.php они не разрешают переменную $ref как таковую. Если я обновил ваш код, как показано ниже

// Alias the schema for the endpoint, so JsonSchema can work with it.
$schemaForPath = aliasSchema($id, $schemaForPath);

if (array_key_exists('items', $schemaForPath))
{
  $schemaForPath['items'] = $factory->getSchemaStorage()->resolveRefSchema((object)$schemaForPath['items']);
}
// Validate the things
$jsonValidator->check($endpointData, (object)$schemaForPath);

и запустить его снова, я получаю необходимые исключения

$ php test2.php
End points does not validate. Violations:

[[0].title] The property title is required

[[1].title] The property title is required

Data: [
    {
        "id": 19874
    },
    {
        "id": 19873
    }
]
End points does not validate. Violations:

[title] The property title is required

Data: {
    "id": 19874
}

Вам нужно либо исправить CollectionConstraint.php, либо обратиться к разработчику репозитория. Или вручную замените свой $ref во всей схеме, как показано выше. Мой код решит проблему, специфичную для вашей схемы, но исправление любой другой схемы не должно быть большой проблемой.

Проблема устранена

person Tarun Lalwani    schedule 21.05.2018
comment
Спасибо за исчерпывающий ответ, он кажется правильным, и я открыл PR для библиотеки......... Вы можете присудить свою награду через 21 час. - person Danack; 21.05.2018
comment
@ Данак, не беспокойся. Разместите ссылку на PR в комментариях здесь, так что это здесь для справки - person Tarun Lalwani; 21.05.2018
comment
Немного преждевременно исправлять justinrainbows/json-schema, хотя эта библиотека несколько устарела с точки зрения поддержки последней спецификации схемы JSON, она по-прежнему надежна и надежна для черновика-04. - person vearutop; 21.05.2018
comment
@Danack, назначение награды должно быть доступно сейчас - person Tarun Lalwani; 22.05.2018
comment
@Danack, также смотрите последнее обновление. Оказывается, вы не должны были вмешиваться в тип схемы. Так что пиар не нужен :-) - person Tarun Lalwani; 22.05.2018

EDIT: Здесь важно то, что предоставленный документ схемы является экземпляром схемы Swagger, в которой используется расширенное подмножество схемы JSON для определения некоторых случаев запроса и ответа. Саму схему Swagger 2.0 можно проверить по ее схеме JSON, но она не может действовать как JSON. Схема для структуры ответа API напрямую.

В случае, если схема объекта совместима со стандартной схемой JSON, вы можете выполнить проверку с помощью валидатора общего назначения, но вы должны предоставить все соответствующие определения, это может быть легко, когда у вас есть абсолютные ссылки, но сложнее для локальных (относительных) ссылок, которые начинаются с #/. IIRC они должны быть определены в локальной схеме.


Проблема здесь в том, что вы пытаетесь использовать ссылки на схемы, отделенные от области разрешения. Я добавил id, чтобы сделать ссылки абсолютными, поэтому не требуется попадание в область видимости.

"$ref": "http://example.com/my-schema#/definitions/Article"

Код ниже работает хорошо.

<?php

require_once __DIR__ . '/vendor/autoload.php';

$swaggerSchemaData = json_decode(<<<'JSON'
{
  "id": "http://example.com/my-schema",
  "swagger": "2.0",
  "info": {
    "termsOfService": "http://swagger.io/terms/",
    "version": "1.0.0",
    "title": "Example api"
  },
  "paths": {
    "/articles": {
      "get": {
        "tags": [
          "article"
        ],
        "summary": "Find all articles",
        "description": "Returns a list of articles",
        "operationId": "getArticleById",
        "produces": [
          "application/json"
        ],
        "responses": {
          "200": {
            "description": "successful operation",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "http://example.com/my-schema#/definitions/Article"
              }
            }
          }
        },
        "parameters": [
        ]
      }
    },
    "/articles/{articleId}": {
      "get": {
        "tags": [
          "article"
        ],
        "summary": "Find article by ID",
        "description": "Returns a single article",
        "operationId": "getArticleById",
        "produces": [
          "application/json"
        ],
        "parameters": [
          {
            "name": "articleId",
            "in": "path",
            "description": "ID of article to return",
            "required": true,
            "type": "integer",
            "format": "int64"
          }
        ],
        "responses": {
          "200": {
            "description": "successful operation",
            "schema": {
              "$ref": "http://example.com/my-schema#/definitions/Article"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "Article": {
      "type": "object",
      "required": [
        "id",
        "title"
      ],
      "properties": {
        "id": {
          "type": "integer",
          "format": "int64"
        },
        "title": {
          "type": "string",
          "description": "The title for the link of the article"
        }
      }
    }
  },
  "schemes": [
    "http"
  ],
  "host": "example.com",
  "basePath": "/",
  "tags": [],
  "securityDefinitions": {
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ]
}
JSON
);



$schemaStorage = new \JsonSchema\SchemaStorage();
$schemaStorage->addSchema('http://example.com/my-schema', $swaggerSchemaData);
$factory = new \JsonSchema\Constraints\Factory($schemaStorage);
$validator = new \JsonSchema\Validator($factory);

$schemaData = $swaggerSchemaData->paths->{"/articles"}->get->responses->{"200"}->schema;

$data = json_decode('[{"id":1},{"id":2,"title":"Title2"}]');
$validator->validate($data, $schemaData);
var_dump($validator->isValid()); // bool(false)
$data = json_decode('[{"id":1,"title":"Title1"},{"id":2,"title":"Title2"}]');
$validator->validate($data, $schemaData);
var_dump($validator->isValid()); // bool(true)
person vearutop    schedule 21.05.2018
comment
вы пытаетесь использовать ссылки на схемы, отделенные от области разрешения. Это может быть правдой, но это не имеет значения. В примере схемы зоомагазина нет абсолютных ссылок, petstore.swagger.io/v2/swagger.json и они не должны быть необходимы. - person Danack; 21.05.2018
comment
Вы можете проверить схему swagger (например, petstore.json) с помощью схемы JSON, но вы не можете напрямую проверить объекты swagger с помощью схемы JSON. Вам нужно либо адаптировать их, либо использовать валидатор Swagger Response/Request. Когда вы пытаетесь извлечь $swaggerData["paths"][$path]['get']["responses"][200]['schema'], вы пропускаете ссылки. Локальная ссылка #/... должна быть определена в локальном документе. - person vearutop; 21.05.2018

Я не уверен, что полностью понимаю ваш код здесь, но у меня есть идея, основанная на некоторых предположениях.

Предполагая, что $typeForEndPoint — это схема, которую вы используете для проверки, ваше ключевое слово item должно быть объектом, а не массивом.

Ключевое слово items может быть массивом или объектом. Если это объект, эта схема применима к каждому элементу в массиве. Если это массив, каждый элемент в этом массиве применим к элементу в той же позиции, что и проверяемый массив.

Это означает, что вы проверяете только первый элемент в массиве.

Если «элементы» являются схемой, проверка завершается успешно, если все элементы в массиве успешно проверяются на соответствие этой схеме.

Если "items" представляет собой массив схем, проверка завершается успешно, если каждый элемент экземпляра проверяется на соответствие схеме в той же позиции, если таковая имеется.

https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.4.1

person Relequestual    schedule 18.05.2018
comment
Я не уверен, что полностью понимаю ваш код, да, я часто это понимаю. Я реорганизовал код, чтобы он был автономным и полным примером сам по себе. - person Danack; 21.05.2018
comment
Итак, похоже, что мой ответ только что поднял проблемы в вашем примере смены. Взгляните еще раз. - person Relequestual; 21.05.2018
comment
Схема действительна и работает должным образом при тестировании вне вашего кода. Что-то еще в игре. Копать. - person Relequestual; 21.05.2018
comment
tbh, у меня есть подозрение, что это может быть просто ошибка или что-то, что не поддерживается в библиотеке JsonSchema, которую я использую. - person Danack; 21.05.2018
comment
Да, я думаю, что это должно быть. Они не используют официальный набор тестов. - person Relequestual; 21.05.2018
comment
Чтобы я мог сослаться на него в выпуске, который я открываю, пожалуйста, не могли бы вы дать мне ссылку на официальный набор тестов, пожалуйста? - person Danack; 21.05.2018
comment
Конечно! github.com/json-schema-org/JSON-Schema-Test- Suite. Мы также запускаем Slack, если у вас есть какие-либо другие вопросы, связанные со схемой JSON, которые можно найти на официальном сайте. - person Relequestual; 21.05.2018
comment
Подтвержденный. Я вижу ряд проблем и PR в отношении использования $ref в определенных ключевых словах. Он должен быть универсальным =/ - person Relequestual; 21.05.2018
comment
Давайте продолжим это обсуждение в чате. - person Relequestual; 21.05.2018
comment
Они не используют официальный набор тестов, но они тестируются третьей стороной с результатом около 95% PASS: github.com/swaggest/php-json-schema-bench/blob/master/ - person vearutop; 21.05.2018
comment
Похоже, они ДЕЛАЮТ, но не публикуют результаты =/ github.com/justinrainbow/json-schema/blob/master/tests/Drafts/ - person Relequestual; 21.05.2018

jsonValidator не любит смешанные ассоциации объектов и массивов, вы можете использовать либо:

$jsonValidator->check($endpointData, $schemaForPath);

or

$jsonValidator->check($endpointData, json_decode(json_encode($schemaForPath)));
person jderusse    schedule 21.05.2018
comment
У них есть проверка в самом коде для того же // make sure $schema is an object if (is_array($schema)) { $schema = self::arrayToObjectRecursive($schema); }, который делает именно то, что вы указали. - person Tarun Lalwani; 21.05.2018
comment
При приведении переменной $schemaForPath к объекту эта проверка больше не выполняется (is_array($schema) возвращает false), поэтому $schema больше не конвертируется в объект. Я указываю либо не преобразовывать массив в объект и позволить библиотеке вызывать self::arrayToObjectRecursive, либо преобразовывать весь массив в объект (что эквивалентно исходному вызову self::arrayToObjectRecursive) - person jderusse; 22.05.2018