代码之家  ›  专栏  ›  技术社区  ›  Danack

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

  •  6
  • Danack  · 技术社区  · 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}"));
    

    最小的作曲家。json是:

    {
        "require": {
            "justinrainbow/json-schema": "^5.2"
        }
    }
    
    4 回复  |  直到 6 年前
        1
  •  3
  •   Tarun Lalwani    6 年前

    编辑-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 或者与回购协议的开发商公开发行。或者手动更换 $参考号 在整个模式中,如上所示。我的代码将解决特定于您的模式的问题,但修复任何其他模式不应该是一个大问题

    Issue fixed

        2
  •  3
  •   vearutop    6 年前

    编辑: 这里重要的是,提供的模式文档是Swagger模式的实例,它采用 extended subset of JSON Schema 定义一些请求和响应案例。Swagger 2.0模式本身可以通过其 JSON Schema ,但它不能直接作为API响应结构的JSON模式。

    如果实体模式与标准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)
    
        3
  •  0
  •   Community Egal    2 年前

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

    假设 $typeForEndPoint 是用于验证的架构 item 关键字必须是对象而不是数组。

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

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

    如果“items”是架构,则如果 数组已成功根据该架构进行验证。

    如果“items”是一个模式数组,那么如果每个元素 实例在相同位置对架构进行验证,如果 任何

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

        4
  •  0
  •   jderusse    6 年前

    jsonValidator不喜欢对象和数组关联的混合, 您可以使用:

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

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