diff --git a/src/JsonSchema/SchemaStorage.php b/src/JsonSchema/SchemaStorage.php index f818aecd..156cc324 100644 --- a/src/JsonSchema/SchemaStorage.php +++ b/src/JsonSchema/SchemaStorage.php @@ -14,6 +14,41 @@ class SchemaStorage implements SchemaStorageInterface { public const INTERNAL_PROVIDED_SCHEMA_URI = 'internal://provided-schema/'; + /** + * JSON Schema keywords that indicate an object is a schema with validation rules, + * not a pure container (like the value of definitions or properties keywords). + * + * Note: 'properties' is a schema keyword (e.g., {"type": "object", "properties": {...}}). + * The properties *container* (the value) won't have these keywords, so it won't match. + * + * Note: 'enum' and 'const' are intentionally excluded as they are handled specially. + */ + private const SCHEMA_KEYWORDS = [ + // Core schema identifiers + '$schema', 'id', '$id', '$comment', 'title', 'description', + + // Type and structure + 'type', 'properties', 'patternProperties', 'additionalProperties', + 'items', 'additionalItems', 'required', 'dependencies', + 'definitions', '$defs', + + // Validation keywords + 'format', 'pattern', + 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', + 'minLength', 'maxLength', + 'minItems', 'maxItems', 'uniqueItems', 'contains', + 'minProperties', 'maxProperties', + + // Composition + 'allOf', 'anyOf', 'oneOf', 'not', + + // Conditionals + 'if', 'then', 'else', + + // Metadata and annotations + 'default', 'examples', 'readOnly', 'writeOnly' + ]; + protected $uriRetriever; protected $uriResolver; protected $schemas = []; @@ -101,7 +136,9 @@ private function expandRefs(&$schema, ?string $parentId = null): void } foreach ($schema as $propertyName => &$member) { - if (in_array($propertyName, ['enum', 'const'])) { + // Only skip enum/const if we're in an actual schema object (has validation keywords), + // not in a container object like definitions where 'enum' might be a property/definition name + if (in_array($propertyName, ['enum', 'const']) && $this->isSchemaObject($schema)) { // Enum and const don't allow $ref as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/445 continue; } @@ -205,7 +242,8 @@ private function scanForSubschemas($schema, string $parentId): void $potentialSubSchemaId = $this->findSchemaIdInObject($potentialSubSchema); if (is_string($potentialSubSchemaId) && property_exists($potentialSubSchema, 'type')) { // Enum and const don't allow id as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/471 - if (in_array($propertyName, ['enum', 'const'])) { + // Only skip if we're in an actual schema context, not when 'enum'/'const' is a property/definition name + if (in_array($propertyName, ['enum', 'const']) && $this->isSchemaObject($schema)) { continue; } @@ -233,4 +271,19 @@ private function findSchemaIdInObject(object $schema): ?string return null; } + + /** + * Check if an object appears to be a JSON Schema object (with validation keywords) + * vs a pure container object (like definitions, properties containers) + */ + private function isSchemaObject(object $schema): bool + { + foreach (self::SCHEMA_KEYWORDS as $keyword) { + if (property_exists($schema, $keyword)) { + return true; + } + } + + return false; + } } diff --git a/tests/SchemaStorageTest.php b/tests/SchemaStorageTest.php index f674b178..20916e1a 100644 --- a/tests/SchemaStorageTest.php +++ b/tests/SchemaStorageTest.php @@ -305,4 +305,46 @@ public function testNoDoubleResolve(): void $schemas['test/schema']->{'$ref'} ); } + + /** + * Test that definitions named 'enum' or 'const' are properly resolved + * Regression test for UnresolvableJsonPointerException when definitions are named 'enum' or 'const' + * Related to: https://github.com/mxr576/oas-validation-issue-on-v6 + */ + public function testDefinitionNamedEnumIsResolved(): void + { + // Schema with a definition named 'enum' that contains a $ref + $schema = (object) [ + 'id' => 'http://example.com/schema.json', + 'definitions' => (object) [ + 'enum' => (object) [ + '$ref' => 'http://json-schema.org/draft-04/schema#/properties/enum' + ], + 'const' => (object) [ + '$ref' => 'http://json-schema.org/draft-04/schema#/properties/const' + ] + ] + ]; + + $uriRetriever = $this->prophesize(\JsonSchema\UriRetrieverInterface::class); + $uriRetriever->retrieve('http://example.com/schema.json') + ->willReturn($schema) + ->shouldBeCalled(); + + $schemaStorage = new SchemaStorage($uriRetriever->reveal()); + $schemaStorage->addSchema('http://example.com/schema.json'); + + // Verify the refs in the 'enum' and 'const' definitions were expanded + $storedSchema = $schemaStorage->getSchema('http://example.com/schema.json'); + + // The $ref should have been expanded to include the full URI + $this->assertStringContainsString( + 'http://json-schema.org/draft-04/schema#', + $storedSchema->definitions->enum->{'$ref'} + ); + $this->assertStringContainsString( + 'http://json-schema.org/draft-04/schema#', + $storedSchema->definitions->const->{'$ref'} + ); + } }