Skip to content

Commit 03f6519

Browse files
committed
Merge branch '4.3' into 4.4
# Conflicts: # CHANGELOG.md
2 parents dee0df1 + 1d641cc commit 03f6519

3 files changed

Lines changed: 99 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@
3838
* [c9e5071d9](https://github.com/api-platform/core/commit/c9e5071d973caedeb9b554f885dedbc89743b93d) feat(symfony): deprecate Symfony Security AccessDeniedException (#8318)
3939
* [cc0ae1254](https://github.com/api-platform/core/commit/cc0ae1254acaf9742c7f899dd24a14d46c89ca6e) feat: support dynamic HTTP response status code via request attribute (#7904)
4040

41+
## v4.3.15
42+
43+
### Bug fixes
44+
45+
* [37dfcb397](https://github.com/api-platform/core/commit/37dfcb397583ff1de4e3d87a75aa068a9997364a) fix(serializer): preserve deserialization path and expected type on IRI type-confusion guard (#8353)
46+
* [7bc11b2fe](https://github.com/api-platform/core/commit/7bc11b2fe29278664ab272964a868ed3a99a2951) fix(serializer): accept union-typed IRI collections on denormalization (#8339)
47+
* [a5761cc17](https://github.com/api-platform/core/commit/a5761cc1793c979c71d46305196eb157a44b6c2a) fix(jsonschema): don't require @id in single-item MCP output schema (#8343)
48+
* [be26bbeb3](https://github.com/api-platform/core/commit/be26bbeb369a5fa1eb055ec666b9e00c0590e366) fix(serializer): forward DiscriminatorMap defaultType in PropertyMetadataLoader (#8346)
49+
4150
## v4.3.14
4251

4352
### Bug fixes

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
@@ -1335,6 +1335,76 @@ public function testUnionTypeCollectionDenormalizationAcceptsAnyMember(): void
13351335
$this->assertInstanceOf(Dummy::class, $actual);
13361336
}
13371337

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

0 commit comments

Comments
 (0)