Skip to content

Commit 2de06db

Browse files
fix(jsonapi): output null on a to-one relationship (api-platform#7686)
Co-authored-by: soyuka <soyuka@users.noreply.github.com>
1 parent 381796e commit 2de06db

3 files changed

Lines changed: 111 additions & 9 deletions

File tree

src/JsonApi/JsonSchema/SchemaFactory.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,12 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
321321
}
322322
$relatedDefinitions[$propertyName] = array_flip($refs);
323323
if ($isOne) {
324-
$relationships[$propertyName]['properties']['data'] = self::RELATION_PROPS;
324+
$relationships[$propertyName]['properties']['data'] = [
325+
'oneOf' => [
326+
['type' => 'null'],
327+
self::RELATION_PROPS,
328+
],
329+
];
325330
continue;
326331
}
327332
$relationships[$propertyName]['properties']['data'] = [

src/JsonApi/Serializer/ItemNormalizer.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -460,23 +460,31 @@ private function getPopulatedRelations(object $object, ?string $format, array $c
460460
$relationshipName = $this->nameConverter->normalize($relationshipName, $context['resource_class'], self::FORMAT, $context);
461461
}
462462

463-
$data[$relationshipName] = [
464-
'data' => [],
465-
];
466-
467-
if (!$attributeValue) {
468-
continue;
469-
}
470-
471463
// Many to one relationship
472464
if ('one' === $relationshipDataArray['cardinality']) {
465+
$data[$relationshipName] = [
466+
'data' => null,
467+
];
468+
469+
if (!$attributeValue) {
470+
continue;
471+
}
472+
473473
unset($attributeValue['data']['attributes']);
474474
$data[$relationshipName] = $attributeValue;
475475

476476
continue;
477477
}
478478

479479
// Many to many relationship
480+
$data[$relationshipName] = [
481+
'data' => [],
482+
];
483+
484+
if (!$attributeValue) {
485+
continue;
486+
}
487+
480488
foreach ($attributeValue as $attributeValueElement) {
481489
if (!isset($attributeValueElement['data'])) {
482490
throw new UnexpectedValueException(\sprintf('The JSON API attribute \'%s\' must contain a "data" key.', $relationshipName));

src/JsonApi/Tests/Serializer/ItemNormalizerTest.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,4 +488,93 @@ public function testDenormalizeRelationIsNotResourceLinkage(): void
488488

489489
$normalizer->denormalize($data, Dummy::class, ItemNormalizer::FORMAT);
490490
}
491+
492+
public function testNormalizeWithNullToOneAndEmptyToManyRelationships(): void
493+
{
494+
$dummy = new Dummy();
495+
$dummy->setId(1);
496+
$dummy->setName('Dummy with relationships');
497+
498+
$propertyNameCollectionFactory = $this->createMock(PropertyNameCollectionFactoryInterface::class);
499+
$propertyNameCollectionFactory->method('create')->willReturn(
500+
new PropertyNameCollection(['id', 'name', 'relatedDummy', 'relatedDummies'])
501+
);
502+
503+
$propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class);
504+
$propertyMetadataFactory->method('create')->willReturnCallback(function ($class, $property) {
505+
return match ($property) {
506+
'id' => (new ApiProperty())->withReadable(true)->withIdentifier(true),
507+
'name' => (new ApiProperty())->withReadable(true),
508+
'relatedDummy' => (new ApiProperty())
509+
->withReadable(true)
510+
->withReadableLink(true)
511+
->withNativeType(Type::nullable(Type::object(RelatedDummy::class))),
512+
'relatedDummies' => (new ApiProperty())
513+
->withReadable(true)
514+
->withReadableLink(true)
515+
->withNativeType(Type::collection(Type::object(ArrayCollection::class), Type::object(RelatedDummy::class))),
516+
default => new ApiProperty(),
517+
};
518+
});
519+
520+
$iriConverter = $this->createMock(IriConverterInterface::class);
521+
$iriConverter->method('getIriFromResource')->willReturn('/dummies/1');
522+
523+
$resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
524+
$resourceClassResolver->method('getResourceClass')->willReturn(Dummy::class);
525+
$resourceClassResolver->method('isResourceClass')->willReturnCallback(fn ($class) => \in_array($class, [Dummy::class, RelatedDummy::class], true));
526+
527+
$propertyAccessor = $this->createMock(PropertyAccessorInterface::class);
528+
$propertyAccessor->method('getValue')->willReturnCallback(function ($object, $property) {
529+
return match ($property) {
530+
'id' => 1,
531+
'name' => 'Dummy with relationships',
532+
'relatedDummy' => null,
533+
'relatedDummies' => [],
534+
default => null,
535+
};
536+
});
537+
538+
$resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
539+
$resourceMetadataCollectionFactory->method('create')->willReturn(
540+
new ResourceMetadataCollection('Dummy', [
541+
(new ApiResource())
542+
->withShortName('Dummy')
543+
->withOperations(new Operations(['get' => (new Get())->withShortName('Dummy')])),
544+
])
545+
);
546+
547+
$serializer = $this->createStub(Serializer::class);
548+
549+
$normalizer = new ItemNormalizer(
550+
$propertyNameCollectionFactory,
551+
$propertyMetadataFactory,
552+
$iriConverter,
553+
$resourceClassResolver,
554+
$propertyAccessor,
555+
null,
556+
null,
557+
[],
558+
$resourceMetadataCollectionFactory,
559+
);
560+
561+
$normalizer->setSerializer($serializer);
562+
563+
$result = $normalizer->normalize($dummy, ItemNormalizer::FORMAT, [
564+
'resources' => [],
565+
'resource_class' => Dummy::class,
566+
]);
567+
568+
$this->assertIsArray($result);
569+
$this->assertArrayHasKey('data', $result);
570+
$this->assertArrayHasKey('relationships', $result['data']);
571+
572+
// Verify to-one relationship with null value returns {"data": null}
573+
$this->assertArrayHasKey('relatedDummy', $result['data']['relationships']);
574+
$this->assertSame(['data' => null], $result['data']['relationships']['relatedDummy']);
575+
576+
// Verify to-many relationship with empty array returns {"data": []}
577+
$this->assertArrayHasKey('relatedDummies', $result['data']['relationships']);
578+
$this->assertSame(['data' => []], $result['data']['relationships']['relatedDummies']);
579+
}
491580
}

0 commit comments

Comments
 (0)