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

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

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

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

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

<?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 JsonSchemaSchemaStorage();
    $id = 'file://example';
    $swaggerClass = getSchema(false);
    $schemaStorage->addSchema($id, $swaggerClass);
    $factory = new JsonSchemaConstraintsFactory($schemaStorage);
    $jsonValidator = new JsonSchemaValidator($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] %sn", $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}"));

минимальный композитора.json is:

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

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
}

Edit-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 во всей схеме, как показано выше. Мой код решит проблему, связанную с вашей схемой, но исправление любой другой схемы не должно быть большой проблемой

Issue fixed


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

если схема сущности совместима со стандартной схемой JSON, вы можете выполнить проверку с помощью general цель валидатора, но вы должны предоставить все соответствующие определения, это может быть легко, когда у вас есть абсолютные ссылки, но сложнее для локальных (относительных) ссылок, которые начинаются с #/. 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)

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

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

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

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

Если" элементы " - это схема, проверка выполняется успешно, если все элементы массив успешно проверяется по этой схеме.

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

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


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

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

или

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