Skip to content

Commit 2abda53

Browse files
authored
fix(serializer): fix union types denormalization fallback after security mismatch (#8333)
1 parent ea1ede5 commit 2abda53

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

src/Serializer/AbstractItemNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ private function getResourceFromIri(string $data, array $context, string $resour
766766

767767
// Type-confusion guard: declared relation class must match the IRI's resource.
768768
if (!is_a($item, $resourceClass)) {
769-
throw new InvalidArgumentException(\sprintf('The iri "%s" does not reference the correct resource.', $data));
769+
throw new NotNormalizableValueException(\sprintf('The iri "%s" does not reference the correct resource.', $data));
770770
}
771771

772772
return $item;

src/Serializer/Tests/AbstractItemNormalizerTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,53 @@ public function testDenormalizeWritableLinks(): void
12381238
$propertyAccessorProphecy->setValue($actual, 'relatedDummiesWithUnionTypes', [0 => $relatedDummy3, 1 => $relatedDummy4])->shouldHaveBeenCalled();
12391239
}
12401240

1241+
public function testUnionTypeDenormalizationFallsThroughAfterTypeConfusionGuardMismatch(): void
1242+
{
1243+
$data = ['relatedDummyUnion' => '/related_dummies/1'];
1244+
$relatedDummy = new RelatedDummy();
1245+
1246+
$propertyNameCollectionFactory = $this->createStub(PropertyNameCollectionFactoryInterface::class);
1247+
$propertyNameCollectionFactory->method('create')->willReturn(new PropertyNameCollection(['relatedDummyUnion']));
1248+
1249+
// Dummy is the mismatching member tried first; RelatedDummy is the one that actually matches.
1250+
$propertyMetadataFactory = $this->createStub(PropertyMetadataFactoryInterface::class);
1251+
$propertyMetadataFactory->method('create')->willReturn(
1252+
(new ApiProperty())
1253+
->withNativeType(Type::union(Type::object(Dummy::class), Type::object(RelatedDummy::class)))
1254+
->withWritable(true)->withWritableLink(true)
1255+
);
1256+
1257+
// Always resolves to a RelatedDummy, no matter which type of the union is being attempted.
1258+
$iriConverter = $this->createStub(IriConverterInterface::class);
1259+
$iriConverter->method('getResourceFromIri')->willReturn($relatedDummy);
1260+
1261+
$resourceClassResolver = $this->createStub(ResourceClassResolverInterface::class);
1262+
$resourceClassResolver->method('isResourceClass')->willReturnMap([
1263+
[Dummy::class, true],
1264+
[RelatedDummy::class, true],
1265+
]);
1266+
$resourceClassResolver->method('getResourceClass')->willReturnMap([
1267+
[null, Dummy::class, Dummy::class],
1268+
[null, RelatedDummy::class, RelatedDummy::class],
1269+
]);
1270+
1271+
$propertyAccessor = $this->createMock(PropertyAccessorInterface::class);
1272+
$propertyAccessor->expects($this->once())
1273+
->method('setValue')
1274+
->with($this->isInstanceOf(Dummy::class), 'relatedDummyUnion', $relatedDummy);
1275+
1276+
$serializer = $this->createStub(SerializerInterface::class);
1277+
1278+
$normalizer = new class($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, null, null, [], null, null) extends AbstractItemNormalizer {};
1279+
$normalizer->setSerializer($serializer);
1280+
1281+
// The first union member (Dummy) should correctly fail and we expect to fallback to the second
1282+
// member (RelatedDummy).
1283+
$actual = $normalizer->denormalize($data, Dummy::class);
1284+
1285+
$this->assertInstanceOf(Dummy::class, $actual);
1286+
}
1287+
12411288
public function testDenormalizeRelationNotFoundReturnsNull(): void
12421289
{
12431290
$data = [

0 commit comments

Comments
 (0)