У меня есть код, который правильно проверяет статью, возвращаемую из конечной точки, которая возвращает отдельные статьи. Я почти уверен, что он работает правильно, так как выдает ошибку проверки, когда я намеренно не включаю обязательное поле в статью.
У меня также есть этот код, который пытается проверить массив статей, возвращаемых из конечной точки, которая возвращает массив статей. Однако я почти уверен, что это работает неправильно, поскольку он всегда говорит, что данные действительны, даже когда я намеренно не включаю обязательное поле в статьи.
Как правильно проверить массив данных на соответствие схеме?
Полный тестовый код приведен ниже как автономный запускаемый тест. Оба теста должны завершиться неудачно, но только один из них.
<?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"
}
}





Я не уверен, что полностью понимаю ваш код здесь, но у меня есть идея, основанная на некоторых предположениях.
Предполагая, что $typeForEndPoint - это схема, которую вы используете для проверки, ваше ключевое слово item должно быть объектом, а не массивом.
Ключевое слово items может быть массивом или объектом. Если это объект, эта схема применима к каждому элементу в массиве. Если это массив, каждый элемент в этом массиве применим к элементу в той же позиции, что и проверяемый массив.
Это означает, что вы проверяете только первый элемент в массиве.
If "items" is a schema, validation succeeds if all elements in the array successfully validate against that schema.
If "items" is an array of schemas, validation succeeds if each element of the instance validates against the schema at the same position, if any.
https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.4.1
Хорошо, похоже, мой ответ только что затронул проблемы в вашем примере выполнения смены. Взглянув еще раз.
Схема действительна и работает должным образом при тестировании вне вашего кода. В игре есть кое-что еще. Копаем.
tbh, у меня есть подозрение, что это может быть просто ошибка или что-то, что не поддерживается в используемой мной библиотеке JsonSchema.
Да, я думаю, это должно быть. Они не используют официальный набор тестов.
Чтобы я мог ссылаться на него в проблеме, которую я открываю, не могли бы вы связать мне официальный набор тестов?
Конечно! github.com/json-schema-org/JSON-Schema-Test-Suite - Мы также запускаем слабину, если у вас есть какие-либо другие вопросы, связанные со схемой JSON, которые можно найти на официальном сайте.
Подтвержденный. Я вижу ряд проблем и PR относительно использования $ ref в определенных ключевых словах. Он должен быть универсальным = /
Позвольте нам продолжить обсуждение в чате.
Они не используют официальный набор тестов, но они протестированы третьей стороной с результатом около 95% ПРОЙДЕН: github.com/swaggest/php-json-schema-bench/blob/master/…
Похоже, они ДЕЙСТВУЮТ, но не публикуют результаты = / github.com/justinrainbow/json-schema/blob/master/tests/Draft s /…
Редактировать-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 во всей схеме, как показано выше. Мой код решит проблему, специфичную для вашей схемы, но исправление любой другой схемы не должно быть большой проблемой.
Спасибо за исчерпывающий ответ, он кажется правильным, и я открыл PR для библиотеки ......... «Вы можете назначить награду через 21 час».
@ Данак, не беспокойся. Разместите ссылку на PR в комментариях здесь, чтобы она была здесь для справки.
Немного преждевременно исправлять justinrainbows/json-schema, хотя эта библиотека несколько устарела с точки зрения поддержки последней спецификации схемы JSON, она по-прежнему надежна и надежна для draft-04.
@Danack, назначение наград должно быть доступно сейчас
@Danack, также смотрите последнее обновление. Оказывается, вы не должны были вмешиваться в приведение типов схемы. Так что пиар не нужен :-)
jsonValidator не любит смешанные ассоциации объектов и массивов, Вы можете использовать:
$jsonValidator->check($endpointData, $schemaForPath);
или же
$jsonValidator->check($endpointData, json_decode(json_encode($schemaForPath)));
У них есть проверка самого кода для того же // make sure $schema is an object if (is_array($schema)) { $schema = self::arrayToObjectRecursive($schema); }, который делает именно то, что вы указали.
При преобразовании переменной $schemaForPath в объект эта проверка больше не выполняется (is_array($schema) возвращает false), поэтому схема $ больше не преобразуется в объект. Я указываю либо не преобразовывать массив в объект и позволить библиотеке вызывать self::arrayToObjectRecursive, либо преобразовывать весь массив в объект (что эквивалентно исходному вызову self::arrayToObjectRecursive)
Обновлено: Здесь важно то, что предоставленный документ схемы является экземпляром схемы 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)
«вы пытаетесь использовать ссылки на схемы, отделенные от области разрешения». Это может быть правдой, но не имеет значения. В примере схемы petstore нет абсолютных ссылок, petstore.swagger.io/v2/swagger.json, и в них нет необходимости.
Вы можете проверить схему swagger (например, petstore.json) с помощью схемы JSON, но вы не можете напрямую проверять сущности swagger с помощью схемы JSON. Вам нужно либо адаптировать их, либо использовать валидатор ответов / запросов Swagger. Когда вы пытаетесь извлечь $swaggerData["paths"][$path]['get']["responses"][200]['schema'], вы упускаете ссылки. Местная ссылка #/... должна быть определена в локальном документе.
"Я не уверен, что полностью понимаю ваш код здесь" да, я часто это понимаю. Я реорганизовал код, чтобы он стал автономным и сам по себе законченным примером.