如何使用JustinRainbow/JsonSchema正确验证对象数组

6
我有一段代码能正确验证从返回单个文章的端点返回的文章。我相信它正常工作,因为当我故意不包含文章中的必填字段时,它会给出一个验证错误。
另外,我还有这段代码,试图验证从返回文章数组的端点返回的文章数组。但是,我相信它并没有正常工作,即使当我故意不包含文章中的必填字段时,它也总是说数据有效。
如何正确地根据架构验证数据数组?
完整的测试代码如下,可作为独立运行的测试。两个测试都应该失败,但只有其中一个失败了。
<?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"
    }
}
4个回答

3

编辑2:5月22日

我已经深入挖掘,结果发现问题是因为您将顶级转换为object

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

你不应该只是这样做,否则一切都会顺利进行的。

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

所以看起来这不是一个 bug,只是使用方式不正确。如果您只是删除 (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(pull request)...... "您可以在21小时内授予奖金。" - Danack
@Danack,别担心。请在这里的评论中发布PR链接,以便参考。 - Tarun Lalwani
现在就修复 justinrainbows/json-schema 有点过早了,虽然这个库在支持 JSON schema 的最新规范方面有些过时,但对于 draft-04 来说仍然是稳定可靠的。 - vearutop
@Danack,赏金任务现在应该已经可用了。 - Tarun Lalwani
@Danack,还有最新的更新。原来你不应该干涉模式的类型转换。所以不需要PR :-) - Tarun Lalwani

3
编辑:这里重要的是提供的模式文档是Swagger Schema的实例,它采用JSON Schema的扩展子集来定义某些请求和响应的情况。Swagger 2.0 Schema本身可以通过其JSON Schema进行验证,但它不能直接作为API响应结构的JSON Schema。

如果实体模式与标准JSON Schema兼容,则可以使用通用验证器执行验证,但必须提供所有相关定义,如果您有绝对引用,那么这很容易,但对于以#/开头的本地(相对)引用,则更加复杂。我记得它们必须在本地模式中定义。


问题在于您尝试使用与解析范围分离的模式引用。我已添加了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)

1
你正在尝试使用与解析范围分离的模式引用。这可能是真的,但这并不重要。示例petstore模式没有绝对引用,http://petstore.swagger.io/v2/swagger.json,也不应该需要它们。 - Danack
您可以使用 JSON schema 验证 swagger 模式(例如 petstore.json),但无法直接验证 swagger 实体。您需要适应它们或使用 Swagger Response/Request 验证器。当您尝试提取 $swaggerData["paths"][$path]['get']["responses"][200]['schema'] 时,会缺少引用。本地引用 #/... 必须在本地文档中定义。 - vearutop

0

我不确定我完全理解你的代码,但是基于一些假设,我有一个想法。

假设$typeForEndPoint是您用于验证的模式,则您的item关键字需要是一个对象而不是一个数组。

items关键字可以是数组或对象。如果它是一个对象,则该模式适用于数组中的每个项目。如果它是一个数组,则该数组中的每个项目都适用于正在验证的项的相同位置。

这意味着您只验证了数组中的第一个项目。

如果“items”是一个模式,则当数组中的所有元素成功验证该模式时,验证成功。

如果“items”是模式数组,则当实例的每个元素在相同位置上验证模式时,验证成功(如果有)。

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


2
“我不确定我完全理解你这里的代码” 是的,我经常遇到这种情况。我已经重构了代码,使其成为一个独立的、完整的示例。 - Danack
说实话,我怀疑这可能只是一个错误或者是JsonSchema库不支持的东西。 - Danack
1
是的,我认为一定是这样。他们没有使用官方测试套件。 - Relequestual
当然可以!https://github.com/json-schema-org/JSON-Schema-Test-Suite - 如果您有任何其他与JSON Schema相关的问题,我们还运行一个Slack,可以在官方网站上找到。 - Relequestual
1
看起来他们确实这样做了,但是没有发布结果 =/ https://github.com/justinrainbow/json-schema/blob/master/tests/Drafts/Draft4Test.php - Relequestual
显示剩余6条评论

0

jsonValidator 不支持对象和数组混合关联,您可以使用以下任一方式:

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

或者

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

他们在代码本身中进行了检查,以确保 $schema 是一个对象。如果 $schema 是一个数组,则会执行 self::arrayToObjectRecursive($schema),这正是你指出的内容。 - Tarun Lalwani
通过将变量$schemaForPath转换为对象,这个检查就不再执行(is_array($schema)返回false),所以$schema不再被转换成对象。我建议要么不将数组强制转换为对象,让库调用self::arrayToObjectRecursive,要么将整个数组转换为对象(与原始调用self::arrayToObjectRecursive等效)。 - jderusse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接