Как правильно проверить массив объектов с помощью 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"
    }
}
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
0
1 692
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

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

Предполагая, что $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

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

Danack 21.05.2018 13:02

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

Relequestual 21.05.2018 14:40

Схема действительна и работает должным образом при тестировании вне вашего кода. В игре есть кое-что еще. Копаем.

Relequestual 21.05.2018 14:51

tbh, у меня есть подозрение, что это может быть просто ошибка или что-то, что не поддерживается в используемой мной библиотеке JsonSchema.

Danack 21.05.2018 14:58

Да, я думаю, это должно быть. Они не используют официальный набор тестов.

Relequestual 21.05.2018 15:04

Чтобы я мог ссылаться на него в проблеме, которую я открываю, не могли бы вы связать мне официальный набор тестов?

Danack 21.05.2018 15:05

Конечно! github.com/json-schema-org/JSON-Schema-Test-Suite - Мы также запускаем слабину, если у вас есть какие-либо другие вопросы, связанные со схемой JSON, которые можно найти на официальном сайте.

Relequestual 21.05.2018 15:06

Подтвержденный. Я вижу ряд проблем и PR относительно использования $ ref в определенных ключевых словах. Он должен быть универсальным = /

Relequestual 21.05.2018 15:14

Позвольте нам продолжить обсуждение в чате.

Relequestual 21.05.2018 15:18

Они не используют официальный набор тестов, но они протестированы третьей стороной с результатом около 95% ПРОЙДЕН: github.com/swaggest/php-json-schema-bench/blob/master/…

vearutop 21.05.2018 17:33

Похоже, они ДЕЙСТВУЮТ, но не публикуют результаты = / github.com/justinrainbow/json-schema/blob/master/tests/Draft‌ s /…

Relequestual 21.05.2018 17:36
Ответ принят как подходящий

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

Issue fixed

Спасибо за исчерпывающий ответ, он кажется правильным, и я открыл PR для библиотеки ......... «Вы можете назначить награду через 21 час».

Danack 21.05.2018 15:52

@ Данак, не беспокойся. Разместите ссылку на PR в комментариях здесь, чтобы она была здесь для справки.

Tarun Lalwani 21.05.2018 15:55

Немного преждевременно исправлять justinrainbows/json-schema, хотя эта библиотека несколько устарела с точки зрения поддержки последней спецификации схемы JSON, она по-прежнему надежна и надежна для draft-04.

vearutop 21.05.2018 16:35

@Danack, назначение наград должно быть доступно сейчас

Tarun Lalwani 22.05.2018 15:44

@Danack, также смотрите последнее обновление. Оказывается, вы не должны были вмешиваться в приведение типов схемы. Так что пиар не нужен :-)

Tarun Lalwani 22.05.2018 15:54

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); }, который делает именно то, что вы указали.

Tarun Lalwani 21.05.2018 17:21

При преобразовании переменной $schemaForPath в объект эта проверка больше не выполняется (is_array($schema) возвращает false), поэтому схема $ больше не преобразуется в объект. Я указываю либо не преобразовывать массив в объект и позволить библиотеке вызывать self::arrayToObjectRecursive, либо преобразовывать весь массив в объект (что эквивалентно исходному вызову self::arrayToObjectRecursive)

jderusse 22.05.2018 14:33

Обновлено: Здесь важно то, что предоставленный документ схемы является экземпляром схемы 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, и в них нет необходимости.

Danack 21.05.2018 17:44

Вы можете проверить схему swagger (например, petstore.json) с помощью схемы JSON, но вы не можете напрямую проверять сущности swagger с помощью схемы JSON. Вам нужно либо адаптировать их, либо использовать валидатор ответов / запросов Swagger. Когда вы пытаетесь извлечь $swaggerData["paths"][$path]['get']["responses"][200]['schem‌​a'], вы упускаете ссылки. Местная ссылка #/... должна быть определена в локальном документе.

vearutop 21.05.2018 17:51

Другие вопросы по теме