Skip to content

Commit 37dfcb3

Browse files
alexndlmclaude
andauthored
fix(serializer): preserve deserialization path and expected type on IRI type-confusion guard (#8353)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Closes #8352
1 parent be26bbe commit 37dfcb3

2 files changed

Lines changed: 90 additions & 1 deletion

File tree

src/Serializer/AbstractItemNormalizer.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,26 @@ private function getResourceFromIri(string $data, array $context, string $resour
773773
: is_a($item, $resourceClass);
774774

775775
if (!$matchesType) {
776-
throw new NotNormalizableValueException(\sprintf('The iri "%s" does not reference the correct resource.', $data));
776+
// Keep this a NotNormalizableValueException so union/intersection denormalization can fall
777+
// through to the next member (see testUnionType*), but build it through the factory so the
778+
// deserialization path and expected type are preserved on the resulting violation. For a
779+
// union/intersection relation, report every accepted class rather than only the one currently
780+
// being attempted.
781+
$expectedTypes = [$resourceClass];
782+
if ($relationType instanceof Type) {
783+
$classNames = [];
784+
foreach ($relationType instanceof CompositeTypeInterface ? $relationType->getTypes() : [$relationType] as $relationMember) {
785+
if ($relationMember instanceof ObjectType) {
786+
$classNames[] = $relationMember->getClassName();
787+
}
788+
}
789+
790+
if ($classNames) {
791+
$expectedTypes = $classNames;
792+
}
793+
}
794+
795+
throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $data), $data, $expectedTypes, $context['deserialization_path'] ?? null, true);
777796
}
778797

779798
return $item;

src/Serializer/Tests/AbstractItemNormalizerTest.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,76 @@ public function testUnionTypeCollectionDenormalizationAcceptsAnyMember(): void
13291329
$this->assertInstanceOf(Dummy::class, $actual);
13301330
}
13311331

1332+
public function testTypeConfusionGuardPreservesPathAndExpectedType(): void
1333+
{
1334+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
1335+
1336+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
1337+
1338+
// The IRI resolves to a Dummy while a RelatedDummy is expected: the type-confusion guard must reject it.
1339+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
1340+
$iriConverterProphecy->getResourceFromIri(Argument::cetera())->willReturn(new Dummy());
1341+
1342+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
1343+
$resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class);
1344+
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true);
1345+
1346+
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
1347+
1348+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
1349+
$serializerProphecy->willImplement(DenormalizerInterface::class);
1350+
1351+
$normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {};
1352+
$normalizer->setSerializer($serializerProphecy->reveal());
1353+
1354+
try {
1355+
$normalizer->denormalize('/dummies/1', RelatedDummy::class, null, [
1356+
'not_normalizable_value_exceptions' => [],
1357+
'deserialization_path' => 'relatedDummy',
1358+
]);
1359+
$this->fail('Expected a NotNormalizableValueException to be thrown.');
1360+
} catch (NotNormalizableValueException $exception) {
1361+
$this->assertSame('Invalid IRI "/dummies/1".', $exception->getMessage());
1362+
$this->assertSame('relatedDummy', $exception->getPath());
1363+
$this->assertSame([RelatedDummy::class], $exception->getExpectedTypes());
1364+
}
1365+
}
1366+
1367+
public function testTypeConfusionGuardReportsAllUnionMemberTypes(): void
1368+
{
1369+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
1370+
1371+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
1372+
1373+
// The IRI resolves to a Dummy while the relation is declared as RelatedDummy|SecuredDummy.
1374+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
1375+
$iriConverterProphecy->getResourceFromIri(Argument::cetera())->willReturn(new Dummy());
1376+
1377+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
1378+
$resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class);
1379+
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true);
1380+
1381+
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
1382+
1383+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
1384+
$serializerProphecy->willImplement(DenormalizerInterface::class);
1385+
1386+
$normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {};
1387+
$normalizer->setSerializer($serializerProphecy->reveal());
1388+
1389+
try {
1390+
$normalizer->denormalize('/dummies/1', RelatedDummy::class, null, [
1391+
'not_normalizable_value_exceptions' => [],
1392+
'deserialization_path' => 'relation',
1393+
'relation_native_type' => Type::union(Type::object(RelatedDummy::class), Type::object(SecuredDummy::class)),
1394+
]);
1395+
$this->fail('Expected a NotNormalizableValueException to be thrown.');
1396+
} catch (NotNormalizableValueException $exception) {
1397+
// A union relation must report every accepted class, not just the first one attempted.
1398+
$this->assertSame([RelatedDummy::class, SecuredDummy::class], $exception->getExpectedTypes());
1399+
}
1400+
}
1401+
13321402
public function testDenormalizeRelationNotFoundReturnsNull(): void
13331403
{
13341404
$data = [

0 commit comments

Comments
 (0)