From fa39a57bb4d2a606cd6e8cb916effc33ed330eb2 Mon Sep 17 00:00:00 2001 From: Runar Sverrisson Date: Fri, 23 Jan 2026 21:52:02 +0000 Subject: [PATCH 1/4] Fix issues with inlining allOf This should fix an issue with nested allOf which would fail when trying to merge since the path was not absolute. So we keep track of the internal parents path through the recursion to ensure the absolute path within the document is kept track of. Also ensure that if models are part of multiple allOf, that they will be merged correctly. --- src/Service/InlineService.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Service/InlineService.php b/src/Service/InlineService.php index f1f3d39..fe34aba 100644 --- a/src/Service/InlineService.php +++ b/src/Service/InlineService.php @@ -97,6 +97,7 @@ private function getRefName(Reference $reference, SchemaType $type): string private function processDocument( Document&WritableDocument $document, ?Reference $rootReference = null, + ?string $parentPath = null, ): Document&WritableDocument { $isAllOfDocument = ($document->get()['allOf'] ?? null) !== null; foreach ($this->findReferences($document, $rootReference) as $ref => $documentPaths) { @@ -104,6 +105,7 @@ private function processDocument( $type = $this->resolveSchemaType($documentPaths); $refName = $this->getRefName($reference, $type); + $nextParentPath = $parentPath; if ( isset($this->components[$type->name][$refName]) && $this->components[$type->name][$refName] @@ -113,6 +115,9 @@ private function processDocument( $schema = ['$ref' => $schema]; } foreach ($documentPaths as $path) { + if (str_contains($path, '/allOf/')) { + $this->allOfPaths[] = ($parentPath ?? '') . substr($path, 0, (int)strpos($path, '/allOf/')); + } $document->set($path, $schema); } continue; @@ -130,6 +135,7 @@ private function processDocument( } else { $referenceDocument = BasicDocument::fromDocument($this->referenceResolver->resolve($reference)); + $nextParentPath .= $documentPaths[0]; } $schemaId = $referenceDocument->get()['$id'] ?? $refName; // reserve schema to avoid infinite recursion @@ -137,13 +143,15 @@ private function processDocument( $this->components[$type->name][$refName] = $this->processDocument( $referenceDocument, $reference, + $nextParentPath, + )->get(); $this->components[$type->name][$refName]['$id'] = $schemaId; // update $ref to new ref foreach ($documentPaths as $path) { if (str_contains($path, '/allOf/')) { - $this->allOfPaths[] = substr($path, 0, (int)strpos($path, '/allOf/')); + $this->allOfPaths[] = $parentPath . substr($path, 0, (int)strpos($path, '/allOf/')); } $document->set($path, $this->components[$type->name][$refName]); } From b3185facea37c330169ffa03d16dd3990e4c0f92 Mon Sep 17 00:00:00 2001 From: Runar Sverrisson Date: Mon, 26 Jan 2026 10:05:50 +0000 Subject: [PATCH 2/4] Improve allOf inlining to handle multiple references The idea is to fix an issue with a model being references in several allOf clauses. So we keep track of each model and the allOf's in that model. When we encounter the same model again, we add all of the model's allOf paths to the allOf collector array for later reference when merging the schema. --- src/Service/InlineService.php | 59 ++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/src/Service/InlineService.php b/src/Service/InlineService.php index fe34aba..a3030f0 100644 --- a/src/Service/InlineService.php +++ b/src/Service/InlineService.php @@ -21,6 +21,8 @@ final class InlineService private array $components = []; /** @var list */ private array $allOfPaths = []; + /** @var array> */ + private array $modelAllOfPaths = []; private readonly DocumentFactory $documentFactory; private LocalReferenceResolver $localReferenceResolver; @@ -114,12 +116,14 @@ private function processDocument( if (is_string($schema)) { $schema = ['$ref' => $schema]; } - foreach ($documentPaths as $path) { - if (str_contains($path, '/allOf/')) { - $this->allOfPaths[] = ($parentPath ?? '') . substr($path, 0, (int)strpos($path, '/allOf/')); - } - $document->set($path, $schema); - } + $document = $this->processDocumentPaths( + $document, + $schema, + $documentPaths, + $refName, + $rootReference?->getName(), + $parentPath + ); continue; } @@ -149,12 +153,14 @@ private function processDocument( $this->components[$type->name][$refName]['$id'] = $schemaId; // update $ref to new ref - foreach ($documentPaths as $path) { - if (str_contains($path, '/allOf/')) { - $this->allOfPaths[] = $parentPath . substr($path, 0, (int)strpos($path, '/allOf/')); - } - $document->set($path, $this->components[$type->name][$refName]); - } + $document = $this->processDocumentPaths( + $document, + $this->components[$type->name][$refName], + $documentPaths, + $refName, + $rootReference?->getName(), + $parentPath + ); } if ($isAllOfDocument) { $merger = new AllOfMerger($document); @@ -164,6 +170,35 @@ private function processDocument( return $document; } + /** + * @param list $documentPaths + */ + private function processDocumentPaths( + Document&WritableDocument $document, + mixed $schema, + array $documentPaths, + string $refName, + ?string $parentName = null, + ?string $parentPath = null, + ): Document&WritableDocument { + $parentName ??= 'root'; + foreach ($documentPaths as $path) { + if (array_key_exists($refName, $this->modelAllOfPaths)) { + foreach ($this->modelAllOfPaths[$refName] as $allOfPath) { + $this->allOfPaths[] = ($parentPath ?? '') . $path . $allOfPath; + } + } + elseif (str_contains($path, '/allOf/')) { + $stripped = substr($path, 0, (int)strpos($path, '/allOf/')); + $this->modelAllOfPaths[$parentName][] = $stripped; + $this->allOfPaths[] = $parentPath . $stripped; + } + $document->set($path, $schema); + } + + return $document; + } + /** * @return array> */ From 107736cfcb049652a77f0f58fbe4ba7ae64851c1 Mon Sep 17 00:00:00 2001 From: Runar Sverrisson Date: Mon, 26 Jan 2026 12:41:50 +0000 Subject: [PATCH 3/4] Fix code style --- src/Service/InlineService.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Service/InlineService.php b/src/Service/InlineService.php index a3030f0..8317c00 100644 --- a/src/Service/InlineService.php +++ b/src/Service/InlineService.php @@ -122,7 +122,7 @@ private function processDocument( $documentPaths, $refName, $rootReference?->getName(), - $parentPath + $parentPath, ); continue; } @@ -148,7 +148,6 @@ private function processDocument( $referenceDocument, $reference, $nextParentPath, - )->get(); $this->components[$type->name][$refName]['$id'] = $schemaId; @@ -159,7 +158,7 @@ private function processDocument( $documentPaths, $refName, $rootReference?->getName(), - $parentPath + $parentPath, ); } if ($isAllOfDocument) { From f0108fc473500d47de67f5733cf90f0d6447bfb0 Mon Sep 17 00:00:00 2001 From: Runar Sverrisson Date: Mon, 26 Jan 2026 12:45:05 +0000 Subject: [PATCH 4/4] Add a test for allOf inliner fixes This test verifies that multiple uses of models in allOf work as expected when using the AllOfMerger. Also ensures that abosulte paths are used when resolving the allOfs in the merger. --- .../inline-allOf-multiple/schema.dist.json | 145 ++++++++++++++++++ .../inline-allOf-multiple/schema.json | 74 +++++++++ .../sub/NestedAllOf.json | 15 ++ .../sub/OtherNestedAllOf.json | 15 ++ .../inline-allOf-multiple/sub/Role.json | 15 ++ .../inline-allOf-multiple/sub/Sub.json | 13 ++ 6 files changed, 277 insertions(+) create mode 100644 tests/inlined-schemas/inline-allOf-multiple/schema.dist.json create mode 100644 tests/inlined-schemas/inline-allOf-multiple/schema.json create mode 100644 tests/inlined-schemas/inline-allOf-multiple/sub/NestedAllOf.json create mode 100644 tests/inlined-schemas/inline-allOf-multiple/sub/OtherNestedAllOf.json create mode 100644 tests/inlined-schemas/inline-allOf-multiple/sub/Role.json create mode 100644 tests/inlined-schemas/inline-allOf-multiple/sub/Sub.json diff --git a/tests/inlined-schemas/inline-allOf-multiple/schema.dist.json b/tests/inlined-schemas/inline-allOf-multiple/schema.dist.json new file mode 100644 index 0000000..52f54cb --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/schema.dist.json @@ -0,0 +1,145 @@ +{ + "title": "Account", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "new", + "verified", + "active", + "blocked", + "deleted" + ], + "readOnly": true + }, + "createdOn": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "anotherRefWithAllOf": { + "$id": "OtherNestedAllOf.json", + "properties": { + "otherNestedAllOf": { + "properties": { + "sub1": { + "type": "string" + }, + "sub2": { + "format": "uuid", + "type": "string" + } + }, + "readOnly": true, + "required": [ + "sub1" + ], + "type": "object" + } + }, + "type": "object" + }, + "blockedUntil": { + "type": "string", + "format": "date-time" + }, + "role": { + "readOnly": true, + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sub2": { + "type": "string" + }, + "externalId": { + "type": "string", + "format": "uuid" + } + } + }, + "complexAllOfMerge": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sub2": { + "type": "string", + "format": "uuid" + }, + "externalId": { + "type": "string", + "format": "uuid" + }, + "sub1": { + "type": "string" + }, + "extraFields": { + "type": "string" + } + }, + "required": [ + "sub1", + "extraFields" + ] + }, + "otherRefWithAllOf": { + "$id": "OtherNestedAllOf.json", + "properties": { + "otherNestedAllOf": { + "properties": { + "sub1": { + "type": "string" + }, + "sub2": { + "format": "uuid", + "type": "string" + } + }, + "readOnly": true, + "required": [ + "sub1" + ], + "type": "object" + } + }, + "type": "object" + }, + "refWithAllOf": { + "$id": "NestedAllOf.json", + "properties": { + "nestedAllOf": { + "properties": { + "sub1": { + "type": "string" + }, + "sub2": { + "format": "uuid", + "type": "string" + } + }, + "readOnly": true, + "required": [ + "sub1" + ], + "type": "object" + } + }, + "type": "object" + }, + "required": [ + "id", + "status" + ] + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/schema.json b/tests/inlined-schemas/inline-allOf-multiple/schema.json new file mode 100644 index 0000000..d6b7566 --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/schema.json @@ -0,0 +1,74 @@ +{ + "title": "Account", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "new", + "verified", + "active", + "blocked", + "deleted" + ], + "readOnly": true + }, + "createdOn": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "blockedUntil": { + "type": "string", + "format": "date-time" + }, + "role": { + "allOf": [ + { + "readOnly": true + }, + { + "$ref": "sub/Role.json" + } + ] + }, + "complexAllOfMerge": { + "allOf": [ + { + "$ref": "sub/Role.json" + }, + { + "$ref": "sub/Sub.json" + }, + { + "properties": { + "extraFields": { + "type": "string" + } + }, + "required": ["extraFields", "sub1"] + } + ] + }, + "refWithAllOf": { + "$ref": "sub/NestedAllOf.json" + }, + "otherRefWithAllOf": { + "$ref": "sub/OtherNestedAllOf.json" + }, + "anotherRefWithAllOf": { + "$ref": "sub/OtherNestedAllOf.json" + }, + "required": [ + "id", + "status" + ] + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/sub/NestedAllOf.json b/tests/inlined-schemas/inline-allOf-multiple/sub/NestedAllOf.json new file mode 100644 index 0000000..ffaf42e --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/sub/NestedAllOf.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "nestedAllOf": { + "allOf": [ + { + "$ref": "Sub.json" + }, + { + "readOnly": true + } + ] + } + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/sub/OtherNestedAllOf.json b/tests/inlined-schemas/inline-allOf-multiple/sub/OtherNestedAllOf.json new file mode 100644 index 0000000..20833e2 --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/sub/OtherNestedAllOf.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "otherNestedAllOf": { + "allOf": [ + { + "$ref": "Sub.json" + }, + { + "readOnly": true + } + ] + } + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/sub/Role.json b/tests/inlined-schemas/inline-allOf-multiple/sub/Role.json new file mode 100644 index 0000000..ecd8182 --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/sub/Role.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sub2": { + "type": "string" + }, + "externalId": { + "type": "string", + "format": "uuid" + } + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/sub/Sub.json b/tests/inlined-schemas/inline-allOf-multiple/sub/Sub.json new file mode 100644 index 0000000..b6a958e --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/sub/Sub.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "sub1": { + "type": "string" + }, + "sub2": { + "type": "string", + "format": "uuid" + } + }, + "required": ["sub1"] +}