diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b8b6cfa..73ea7e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Fix objects are non-unique despite key order ([#819](https://github.com/jsonrainbow/json-schema/pull/819)) ## [6.4.1] - 2025-04-04 ### Fixed diff --git a/src/JsonSchema/Constraints/CollectionConstraint.php b/src/JsonSchema/Constraints/CollectionConstraint.php index da0e7150..e42a6fb8 100644 --- a/src/JsonSchema/Constraints/CollectionConstraint.php +++ b/src/JsonSchema/Constraints/CollectionConstraint.php @@ -13,6 +13,7 @@ use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; +use JsonSchema\Tool\DeepComparer; /** * The CollectionConstraint Constraints, validates an array against a given schema @@ -39,14 +40,14 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n // Verify uniqueItems if (isset($schema->uniqueItems) && $schema->uniqueItems) { - $unique = $value; - if (is_array($value) && count($value)) { - $unique = array_map(function ($e) { - return var_export($e, true); - }, $value); - } - if (count(array_unique($unique)) != count($value)) { - $this->addError(ConstraintError::UNIQUE_ITEMS(), $path); + $count = count($value); + for ($x = 0; $x < $count - 1; $x++) { + for ($y = $x + 1; $y < $count; $y++) { + if (DeepComparer::isEqual($value[$x], $value[$y])) { + $this->addError(ConstraintError::UNIQUE_ITEMS(), $path); + break 2; + } + } } } diff --git a/tests/Constraints/UniqueItemsTest.php b/tests/Constraints/UniqueItemsTest.php index 7a2444ca..0badfe1d 100644 --- a/tests/Constraints/UniqueItemsTest.php +++ b/tests/Constraints/UniqueItemsTest.php @@ -16,47 +16,51 @@ class UniqueItemsTest extends BaseTestCase public function getInvalidTests(): array { return [ - [ - '[1,2,2]', - '{ + 'Non unique integers' => [ + 'input' => '[1,2,2]', + 'schema' => '{ "type":"array", "uniqueItems": true }' ], - [ - '[{"a":"b"},{"a":"c"},{"a":"b"}]', - '{ + 'Non unique objects' => [ + 'input' => '[{"a":"b"},{"a":"c"},{"a":"b"}]', + 'schema' => '{ "type":"array", "uniqueItems": true }' ], - [ - '[{"foo": {"bar" : {"baz" : true}}}, {"foo": {"bar" : {"baz" : true}}}]', - '{ + 'Non unique objects - three levels deep' => [ + 'input' => '[{"foo": {"bar" : {"baz" : true}}}, {"foo": {"bar" : {"baz" : true}}}]', + 'schema' => '{ "type": "array", "uniqueItems": true }' ], - [ - '[1.0, 1.00, 1]', - '{ + 'Non unique mathematical values for the number one' => [ + 'input' => '[1.0, 1.00, 1]', + 'schema' => '{ "type": "array", "uniqueItems": true }' ], - [ - '[["foo"], ["foo"]]', - '{ + 'Non unique arrays' => [ + 'input' => '[["foo"], ["foo"]]', + 'schema' => '{ "type": "array", "uniqueItems": true }' ], - [ - '[{}, [1], true, null, {}, 1]', - '{ + 'Non unique mix of different types' => [ + 'input' => '[{}, [1], true, null, {}, 1]', + 'schema' => '{ "type": "array", "uniqueItems": true }' + ], + 'objects are non-unique despite key order' => [ + 'input' => '[{"a": 1, "b": 2}, {"b": 2, "a": 1}]', + 'schema' => '{"uniqueItems": true}', ] ]; } @@ -64,94 +68,94 @@ public function getInvalidTests(): array public function getValidTests(): array { return [ - [ - '[1,2,3]', - '{ - "type":"array", - "uniqueItems": true + 'unique integers' => [ + 'input' => '[1,2,3]', + 'schema' => '{ + "type":"array", + "uniqueItems": true }' ], - [ - '[{"foo": 12}, {"bar": false}]', - '{ + 'unique objects' =>[ + 'input' => '[{"foo": 12}, {"bar": false}]', + 'schema' => '{ "type": "array", "uniqueItems": true }' ], - [ - '[1, true]', - '{ + 'Integer one and boolean true' => [ + 'input' => '[1, true]', + 'schema' => '{ "type": "array", "uniqueItems": true }' ], - [ - '[0, false]', - '{ + 'Integer zero and boolean false' => [ + 'input' => '[0, false]', + 'schema' => '{ "type": "array", "uniqueItems": true }' ], - [ - '[{"foo": {"bar" : {"baz" : true}}}, {"foo": {"bar" : {"baz" : false}}}]', - '{ + 'Objects with different value three levels deep' => [ + 'input' => '[{"foo": {"bar" : {"baz" : true}}}, {"foo": {"bar" : {"baz" : false}}}]', + 'schema' => '{ "type": "array", "uniqueItems": true }' ], - [ - '[["foo"], ["bar"]]', - '{ + 'Array of strings' => [ + 'input' => '[["foo"], ["bar"]]', + 'schema' => '{ "type": "array", "uniqueItems": true }' ], - [ - '[{}, [1], true, null, 1]', - '{ + 'Object, Array, boolean, null and integer' => [ + 'input' => '[{}, [1], true, null, 1]', + 'schema' => '{ "type": "array", "uniqueItems": true }' ], // below equals the invalid tests, but with uniqueItems set to false - [ - '[1,2,2]', - '{ + 'Non unique integers' => [ + 'input' => '[1,2,2]', + 'schema' => '{ "type":"array", "uniqueItems": false }' ], - [ - '[{"a":"b"},{"a":"c"},{"a":"b"}]', - '{ + 'Non unique objects' => [ + 'input' => '[{"a":"b"},{"a":"c"},{"a":"b"}]', + 'schema' => '{ "type":"array", "uniqueItems": false }' ], - [ - '[{"foo": {"bar" : {"baz" : true}}}, {"foo": {"bar" : {"baz" : true}}}]', - '{ + 'Non unique objects - three levels deep' => [ + 'input' => '[{"foo": {"bar" : {"baz" : true}}}, {"foo": {"bar" : {"baz" : true}}}]', + 'schema' => '{ "type": "array", "uniqueItems": false }' ], - [ - '[1.0, 1.00, 1]', - '{ + 'Non unique mathematical values for the number one' => [ + 'input' => '[1.0, 1.00, 1]', + 'schema' => '{ "type": "array", "uniqueItems": false }' ], - [ - '[["foo"], ["foo"]]', - '{ + 'Non unique arrays' => [ + 'input' => '[["foo"], ["foo"]]', + 'schema' => '{ "type": "array", "uniqueItems": false }' ], - [ - '[{}, [1], true, null, {}, 1]', - '{ + 'Non unique mix of different types' => [ + 'input' => '[{}, [1], true, null, {}, 1]', + 'schema' => '{ "type": "array", "uniqueItems": false }'