swaggest / php-json-schema

High definition PHP structures with JSON-schema based validation
MIT License
438 stars 50 forks source link

Problems with top level oneOf #123

Open ghost opened 3 years ago

ghost commented 3 years ago

I have the following schema:

    public static function setUpProperties($properties, Schema $ownerSchema)
    {
        $ownerSchema->oneOf = [
            Object1::schema(),
            Object2::schema(),
            Object3::schema()
        ];

        $ownerSchema->setFromRef(self::className());
        $ownerSchema->additionalProperties = false;
    }

where Object1..3 extend from a base object and have a discriminator property that changes a type specific section according to its value. Currently I have the problem that despite the oneOf validation and import working correctly afterwards processObject is called in https://github.com/swaggest/php-json-schema/blob/master/src/Schema.php#L1204-L1206. Then the validator complains about additional properties because I set those to false. If I remove that then I of course get a object back which consists only of additional properties.

My question is now if the thing I try to do here is valid (in JSON Schema) and whether I'm doing something wrong. Or if the additional invokation of processObject should be only done as fallback if nothing else matched before.

vearutop commented 3 years ago

Hi, additionalProperties ignore properties defined in sub schemas (like in oneOf/...). If you want to disallow additionalProperties, I'd suggest to move it into every oneOf sub schema.

Another solution could be to name properties from all sub schemas in root schema.

{
 "oneOf": [
   {"properties": {"a": {"type":"string", "b": {"type":"integer"}}}, "required": ["a","b"]},
   {"properties": {"c": {"type":"string", "d": {"type":"integer"}}}, "required": ["c","d"]},
   {"properties": {"e": {"type":"string", "f": {"type":"integer"}}}, "required": ["e","f"]}
 ],
 "properties": {"a":{},"b":{},"c":{},"d":{},"e":{},"f":{}},
 "additionalProperties": false
}

Alternatively, latest JSON schema draft has a concept of unevaluatedProperties, but unfortunately this keyword is not yet supported by swaggest/json-schema.

ghost commented 3 years ago

Thanks, I was absent hence the late reply. Either setting additionalProperties to true or redefining properties on the parent class would solve the problem that the validator complains about additional properties. However, this unfortunately does not solve my problem because I'm relying on the type of the returned object. Since the parent is oneOf I would expect that the result would be either Object1, Object2 or Object3. With the approaches to circumvent the additionalProperties problem I get the parent class type back.

Again I'm not sure if I'm trying something which is not right or not supported. I'm attaching a minimal example which maybe helps to see better what I want to achieve.

<?php
require_once __DIR__ . '/vendor/autoload.php';

use Swaggest\JsonSchema\Structure\ClassStructure;
use Swaggest\JsonSchema\Schema;

class ParentObject extends ClassStructure
{
    public static function setUpProperties($properties, Schema $ownerSchema)
    {
        $ownerSchema->oneOf = [
            ChildObject1::schema(),
            ChildObject2::schema()
        ];

        $ownerSchema->setFromRef(self::className());
        // remove comment to receive Swaggest\JsonSchema\Exception\ObjectException: Additional properties not allowed
        // $ownerSchema->additionalProperties = false;
    }
}

class BaseObject extends ClassStructure
{
    public $baseAttr1;
    public $baseAttr2;

    public static function setUpProperties($properties, Schema $ownerSchema)
    {
        $properties->baseAttr1 = Schema::string();
        $properties->baseAttr2 = Schema::integer();

        $ownerSchema->type = 'object';
        $ownerSchema->setFromRef(self::className());
        $ownerSchema->required = [
            self::names()->baseAttr1
        ];
        $ownerSchema->additionalProperties = false;
    }
}

class ChildObject1 extends BaseObject
{
    public $typeSpecific;

    public static function setUpProperties($properties, Schema $ownerSchema)
    {
        parent::setUpProperties($properties, $ownerSchema);
        $properties->typeSpecific = ChildObject1TypeSpecific::schema();
        $ownerSchema->required[] = self::names()->typeSpecific;
    }
}

class ChildObject1TypeSpecific extends ClassStructure
{
    public $child1Attr;

    public static function setUpProperties($properties, Schema $ownerSchema)
    {
        $properties->child1Attr = Schema::integer();

        $ownerSchema->type = 'object';
        $ownerSchema->setFromRef(self::className());
        $ownerSchema->required = [
            self::names()->child1Attr
        ];
        $ownerSchema->additionalProperties = true;
    }
}

class ChildObject2 extends BaseObject
{
    public $typeSpecific;

    public static function setUpProperties($properties, Schema $ownerSchema)
    {
        parent::setUpProperties($properties, $ownerSchema);
        $properties->typeSpecific = ChildObject2TypeSpecific::schema();
        $ownerSchema->required[] = self::names()->typeSpecific;
    }
}

class ChildObject2TypeSpecific extends ClassStructure
{
    public $child2Attr;

    public static function setUpProperties($properties, Schema $ownerSchema)
    {
        $properties->child2Attr = Schema::string();

        $ownerSchema->type = 'object';
        $ownerSchema->setFromRef(self::className());
        $ownerSchema->required = [
            self::names()->child2Attr
        ];
        $ownerSchema->additionalProperties = true;
    }
}

$input = <<<JSON
{
    "baseAttr1": "baseAttr1",
    "baseAttr2": 1,
    "typeSpecific": {
        "child2Attr": "child2Attr"
    }
}
JSON;

$imported = ParentObject::import(json_decode($input));
echo get_class($imported); // = ParentObject but expecting ChildObject2