From 8f77afedd2bc076031d4d4cc1a8ad1ad0a892862 Mon Sep 17 00:00:00 2001 From: Sebastian Breuers Date: Wed, 29 Apr 2026 16:48:46 +0200 Subject: [PATCH 1/2] fix: converting collections breaks since it does not resolve to element class --- src/JsonApi/Serializer/ItemNormalizer.php | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index 4d9d6e225c..e55d57f675 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -266,7 +266,7 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v * @throws RuntimeException * @throws UnexpectedValueException */ - protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object + protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $targetClass, mixed $value, ?string $format, array $context): ?object { if (!\is_array($value) || !isset($value['id'], $value['type'])) { throw new UnexpectedValueException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.'); @@ -278,19 +278,6 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope return $this->iriConverter->getResourceFromIri($value['id'], $context); } - $targetClass = null; - $nativeType = $propertyMetadata->getNativeType(); - - if ($nativeType) { - $nativeType->isSatisfiedBy(function (Type $type) use (&$targetClass): bool { - return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($targetClass = $type->getClassName()); - }); - } - - if (null === $targetClass) { - throw new ItemNotFoundException(\sprintf('Cannot determine target class for property "%s".', $attributeName)); - } - /** @var HttpOperation $getOperation */ $getOperation = $this->resourceMetadataCollectionFactory->create($targetClass)->getOperation(httpOperation: true); $iri = $this->reconstructIri($targetClass, (string) $value['id'], $getOperation); @@ -303,7 +290,7 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope $context['not_normalizable_value_exceptions'][] = NotNormalizableValueException::createForUnexpectedDataType( $e->getMessage(), $value, - [$className], + [$targetClass], $context['deserialization_path'] ?? null, true, $e->getCode(), From 155ecb970051198aa57fe662ca35eebda79402ec Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 30 Apr 2026 10:50:52 +0200 Subject: [PATCH 2/2] rename prop + add non reg test --- src/JsonApi/Serializer/ItemNormalizer.php | 8 +-- .../Tests/Serializer/ItemNormalizerTest.php | 61 +++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index e55d57f675..b97c1411dc 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -266,7 +266,7 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v * @throws RuntimeException * @throws UnexpectedValueException */ - protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $targetClass, mixed $value, ?string $format, array $context): ?object + protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object { if (!\is_array($value) || !isset($value['id'], $value['type'])) { throw new UnexpectedValueException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.'); @@ -279,8 +279,8 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope } /** @var HttpOperation $getOperation */ - $getOperation = $this->resourceMetadataCollectionFactory->create($targetClass)->getOperation(httpOperation: true); - $iri = $this->reconstructIri($targetClass, (string) $value['id'], $getOperation); + $getOperation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(httpOperation: true); + $iri = $this->reconstructIri($className, (string) $value['id'], $getOperation); return $this->iriConverter->getResourceFromIri($iri, $context); } catch (ItemNotFoundException $e) { @@ -290,7 +290,7 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope $context['not_normalizable_value_exceptions'][] = NotNormalizableValueException::createForUnexpectedDataType( $e->getMessage(), $value, - [$targetClass], + [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), diff --git a/src/JsonApi/Tests/Serializer/ItemNormalizerTest.php b/src/JsonApi/Tests/Serializer/ItemNormalizerTest.php index 683ed89cd3..fd4b2ea12e 100644 --- a/src/JsonApi/Tests/Serializer/ItemNormalizerTest.php +++ b/src/JsonApi/Tests/Serializer/ItemNormalizerTest.php @@ -314,6 +314,67 @@ public function testDenormalize(): void $this->assertInstanceOf(Dummy::class, $normalizer->denormalize($data, Dummy::class, ItemNormalizer::FORMAT)); } + // https://github.com/api-platform/core/pull/7938 + public function testDenormalizeRelationUsesClassNameArgument(): void + { + $relatedDummy = new RelatedDummy(); + $relatedDummy->setId(1); + + $propertyMetadata = (new ApiProperty()) + ->withNativeType(Type::collection(Type::object(ArrayCollection::class), Type::object(RelatedDummy::class), Type::int())) + ->withReadable(false)->withWritable(true) + ->withReadableLink(false)->withWritableLink(false); + + $iriConverter = $this->createStub(IriConverterInterface::class); + $iriConverter->method('getIriFromResource')->willReturn('/related_dummies/1'); + $iriConverter->method('getResourceFromIri')->willReturn($relatedDummy); + + $resourceClassResolver = $this->createStub(ResourceClassResolverInterface::class); + $resourceClassResolver->method('isResourceClass')->willReturnMap([ + [RelatedDummy::class, true], + [ArrayCollection::class, false], + ]); + + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->expects($this->once()) + ->method('create') + ->with(RelatedDummy::class) + ->willReturn(new ResourceMetadataCollection(RelatedDummy::class, [ + (new ApiResource())->withOperations(new Operations([ + new Get(name: 'get', uriTemplate: '/related_dummies/{id}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'])]), + ])), + ])); + + $normalizer = new ItemNormalizer( + $this->createStub(PropertyNameCollectionFactoryInterface::class), + $this->createStub(PropertyMetadataFactoryInterface::class), + $iriConverter, + $resourceClassResolver, + null, + new ReservedAttributeNameConverter(), + null, + [], + $resourceMetadataCollectionFactory, + null, + null, + null, + null, + false, + ); + + $result = (new \ReflectionMethod(ItemNormalizer::class, 'denormalizeRelation'))->invoke( + $normalizer, + 'relatedDummies', + $propertyMetadata, + RelatedDummy::class, + ['type' => 'related-dummy', 'id' => '1'], + null, + [] + ); + + $this->assertSame($relatedDummy, $result); + } + public function testDenormalizeUpdateOperationNotAllowed(): void { $this->expectException(NotNormalizableValueException::class);