From 1dcc0bd9a65ae9b77d5dc2fb6e68bb7ac86539ba Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 14 Aug 2026 09:54:26 +0530 Subject: [PATCH] fix: capture extensions on the discriminator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONTEXT.md states that an extension is "captured on every model that can carry one". Discriminator was the exception: the reader built it from propertyName and mapping and dropped every x- key alongside them, so an extension nested inside a discriminator was unreachable from the parsed Specification. This matters because the standard mapping is single-valued — it maps one property value to one schema. A union whose members are told apart by a combination of properties cannot be expressed with it. Appwrite's spec carries the full rule set in `x-mapping`: "discriminator": { "propertyName": "type", "mapping": { "string": ".../attributeString" }, "x-mapping": { ".../attributeEmail": { "type": "string", "format": "email" }, ".../attributeString": { "type": "string" } } } Five of its attribute models share `type: "string"` and differ only by `format`, so `mapping` can name just one of them and consumers reading only `mapping` silently collapse the other four into it. The reader already has Value::extensions(); this passes its result to the constructor. Nothing here interprets the extension — it is captured and handed to the consumer, which is the contract every other model follows. --- src/Model/Schema/Discriminator.php | 6 +++- src/Parser/Schema/Reader.php | 6 +++- tests/Schema/ReaderTest.php | 46 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/Model/Schema/Discriminator.php b/src/Model/Schema/Discriminator.php index 0778e5e..9182c8f 100644 --- a/src/Model/Schema/Discriminator.php +++ b/src/Model/Schema/Discriminator.php @@ -6,9 +6,13 @@ final readonly class Discriminator { - /** @param array $mapping */ + /** + * @param array $mapping + * @param array $extensions + */ public function __construct( public string $propertyName, public array $mapping = [], + public array $extensions = [], ) {} } diff --git a/src/Parser/Schema/Reader.php b/src/Parser/Schema/Reader.php index c10014f..70ba9bc 100644 --- a/src/Parser/Schema/Reader.php +++ b/src/Parser/Schema/Reader.php @@ -263,6 +263,10 @@ private function discriminator(array $data): ?Discriminator $mapping[(string) $name] = $reference; } - return new Discriminator(Value::requiredString($value, 'propertyName', 'schema/discriminator'), $mapping); + return new Discriminator( + Value::requiredString($value, 'propertyName', 'schema/discriminator'), + $mapping, + Value::extensions($value), + ); } } diff --git a/tests/Schema/ReaderTest.php b/tests/Schema/ReaderTest.php index 5d206ef..228edda 100644 --- a/tests/Schema/ReaderTest.php +++ b/tests/Schema/ReaderTest.php @@ -115,6 +115,52 @@ public function test_discriminator_is_read_from_both_the_string_and_object_forms self::assertSame(['cat' => '#/components/schemas/Cat'], $fromObject->discriminator?->mapping); } + public function test_discriminator_captures_extensions(): void + { + $reader = $this->reader(Version::V3_0); + + $schema = $reader->read([ + 'oneOf' => [], + 'discriminator' => [ + 'propertyName' => 'type', + 'mapping' => ['string' => '#/components/schemas/Text'], + 'x-mapping' => [ + '#/components/schemas/Email' => ['type' => 'string', 'format' => 'email'], + '#/components/schemas/Text' => ['type' => 'string'], + ], + 'x-propertyNames' => ['type', 'format'], + ], + ], '#/x'); + + self::assertInstanceOf(CompositeSchema::class, $schema); + self::assertSame([ + 'x-mapping' => [ + '#/components/schemas/Email' => ['type' => 'string', 'format' => 'email'], + '#/components/schemas/Text' => ['type' => 'string'], + ], + 'x-propertyNames' => ['type', 'format'], + ], $schema->discriminator?->extensions); + + self::assertSame('type', $schema->discriminator?->propertyName); + self::assertSame(['string' => '#/components/schemas/Text'], $schema->discriminator?->mapping); + } + + public function test_discriminator_extensions_default_to_empty(): void + { + $reader = $this->reader(Version::V3_0); + + $fromString = $reader->read(['oneOf' => [], 'discriminator' => 'kind'], '#/x'); + self::assertInstanceOf(CompositeSchema::class, $fromString); + self::assertSame([], $fromString->discriminator?->extensions); + + $fromObject = $reader->read([ + 'oneOf' => [], + 'discriminator' => ['propertyName' => 'kind'], + ], '#/x'); + self::assertInstanceOf(CompositeSchema::class, $fromObject); + self::assertSame([], $fromObject->discriminator?->extensions); + } + public function test_object_and_array_types_are_implied_from_their_keywords(): void { $reader = $this->reader(Version::V3_0);