From afbb3f0a8eacb762b93d566702f18bf9bc8f3d05 Mon Sep 17 00:00:00 2001 From: Jonathan Eom Date: Thu, 18 Jun 2026 11:22:33 -0400 Subject: [PATCH 1/2] Add test to break UnionType denormalization after type confusion guard --- .../Tests/AbstractItemNormalizerTest.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 55eb5dcdad2..89ea53f45bf 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -1238,6 +1238,53 @@ public function testDenormalizeWritableLinks(): void $propertyAccessorProphecy->setValue($actual, 'relatedDummiesWithUnionTypes', [0 => $relatedDummy3, 1 => $relatedDummy4])->shouldHaveBeenCalled(); } + public function testUnionTypeDenormalizationFallsThroughAfterTypeConfusionGuardMismatch(): void + { + $data = ['relatedDummyUnion' => '/related_dummies/1']; + $relatedDummy = new RelatedDummy(); + + $propertyNameCollectionFactory = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactory->method('create')->willReturn(new PropertyNameCollection(['relatedDummyUnion'])); + + // Dummy is the mismatching member tried first; RelatedDummy is the one that actually matches. + $propertyMetadataFactory = $this->createStub(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory->method('create')->willReturn( + (new ApiProperty()) + ->withNativeType(Type::union(Type::object(Dummy::class), Type::object(RelatedDummy::class))) + ->withWritable(true)->withWritableLink(true) + ); + + // Always resolves to a RelatedDummy, no matter which type of the union is being attempted. + $iriConverter = $this->createStub(IriConverterInterface::class); + $iriConverter->method('getResourceFromIri')->willReturn($relatedDummy); + + $resourceClassResolver = $this->createStub(ResourceClassResolverInterface::class); + $resourceClassResolver->method('isResourceClass')->willReturnMap([ + [Dummy::class, true], + [RelatedDummy::class, true], + ]); + $resourceClassResolver->method('getResourceClass')->willReturnMap([ + [null, Dummy::class, Dummy::class], + [null, RelatedDummy::class, RelatedDummy::class], + ]); + + $propertyAccessor = $this->createMock(PropertyAccessorInterface::class); + $propertyAccessor->expects($this->once()) + ->method('setValue') + ->with($this->isInstanceOf(Dummy::class), 'relatedDummyUnion', $relatedDummy); + + $serializer = $this->createStub(SerializerInterface::class); + + $normalizer = new class($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, null, null, [], null, null) extends AbstractItemNormalizer {}; + $normalizer->setSerializer($serializer); + + // The first union member (Dummy) should correctly fail and we expect to fallback to the second + // member (RelatedDummy). + $actual = $normalizer->denormalize($data, Dummy::class); + + $this->assertInstanceOf(Dummy::class, $actual); + } + public function testDenormalizeRelationNotFoundReturnsNull(): void { $data = [ From 1811157f1c601c0a16a6f471930483ada16cc8a2 Mon Sep 17 00:00:00 2001 From: Jonathan Eom Date: Thu, 18 Jun 2026 11:30:00 -0400 Subject: [PATCH 2/2] fix(serializer): denormalize union types with security mismatch --- src/Serializer/AbstractItemNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index a9dd3f5e068..75361cecea0 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -766,7 +766,7 @@ private function getResourceFromIri(string $data, array $context, string $resour // Type-confusion guard: declared relation class must match the IRI's resource. if (!is_a($item, $resourceClass)) { - throw new InvalidArgumentException(\sprintf('The iri "%s" does not reference the correct resource.', $data)); + throw new NotNormalizableValueException(\sprintf('The iri "%s" does not reference the correct resource.', $data)); } return $item;