JSON:API: relations set through the constructor reject spec resource linkage (`relationships.<name>.data`) · Issue #8702 · api-platform/core · GitHub
Skip to content

JSON:API: relations set through the constructor reject spec resource linkage (relationships.<name>.data) #8702

Closed

Description

@aszenz

API Platform version(s) affected

4.4.4

Description

When a relation is set through a constructor argument, the JSON:API denormalizer does not accept the resource linkage shape of the spec (relationships.<name>.data).

  • To-one with {"data": {"type": ..., "id": ...}} or {"data": null}: 400 "Only resource linkage supported currently".
  • To-many with {"data": [...]}: 400 "The type of the key "data" must be "int", "string" given."
  • To-one with the flat shape {"type": ..., "id": ...}, which is not the spec shape: 201.

The same relation as a public property (no constructor) accepts the spec shape.

The cause looks like this: JsonApi\Serializer\ItemNormalizerTrait::setAttributeValue() removes the data wrapper, but AbstractItemNormalizer::createConstructorArgument() calls createAndValidateAttributeValue() directly. The JSON:API normalizer does not override it, so the value still has the data wrapper when it gets to denormalizeRelation().

How to reproduce

#[ApiResource(operations: [new Get(provider: [self::class, 'provide'])])]
final class Author
{
    public function __construct(public int $id = 1, public string $name = 'Frank Herbert') {}
    public static function provide(): self { return new self(); }
}

#[ApiResource(operations: [
    new Get(provider: [self::class, 'provide']),
    new Post(processor: [self::class, 'process']),
])]
final class Book
{
    /** @param list<Author> $coAuthors */
    public function __construct(
        public int $id = 1,
        public string $title = 'Dune',
        public ?Author $author = null,
        public array $coAuthors = [],
    ) {}

    public static function provide(): ?self { return null; }
    public static function process(self $data, Operation $operation): self { return $data; }
}

// Same relation, set as a property
#[ApiResource(operations: [
    new Get(provider: [self::class, 'provide']),
    new Post(processor: [self::class, 'process']),
])]
final class Novel
{
    public int $id = 1;
    public string $title = 'Dune';
    public ?Author $author = null;

    public static function provide(): self { return new self(); }
    public static function process(self $data, Operation $operation): self { return $data; }
}
POST relationships Status
/books {"author":{"data":{"type":"Author","id":"/authors/1"}}} 400
/books {"author":{"data":null}} 400
/books {"coAuthors":{"data":[{"type":"Author","id":"/authors/1"}]}} 400
/books {"author":{"type":"Author","id":"/authors/1"}} (not spec) 201
/novels {"author":{"data":{"type":"Author","id":"/authors/1"}}} 201

Full request for the first row:

POST /books

{"data":{"type":"Book","attributes":{"title":"A"},"relationships":{"author":{"data":{"type":"Author","id":"/authors/1"}}}}}
{"errors":[{"id":"/errors/400","title":"An error occurred","detail":"Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.","status":400,"type":"/errors/400","links":{"type":"/errors/400"},"code":"400"}]}

Expected: a relation set through the constructor accepts the same relationships.<name>.data shape as a relation set through a property. The flat shape is not accepted.

Possible Solution

Remove the data wrapper for constructor arguments too, for example with a createConstructorArgument() override in ItemNormalizerTrait that does what setAttributeValue() does.

Additional Context

Checked on api-platform/core v4.4.4, Symfony 7.4, PHP 8.5, in an empty Symfony app. The related code in src/JsonApi/Serializer is the same on main, except for the error status cast (#8366).

The app has only FrameworkBundle, TwigBundle and ApiPlatformBundle, with this config:

$c->extension('api_platform', [
    'mapping' => ['paths' => ['%kernel.project_dir%/src/ApiResource']],
    'formats' => ['jsonapi' => ['application/vnd.api+json']],
    'error_formats' => ['jsonapi' => ['application/vnd.api+json'], 'jsonproblem' => ['application/problem+json']],
    'doctrine' => ['enabled' => false],
]);

All requests send Accept: application/vnd.api+json and, when they have a body, Content-Type: application/vnd.api+json. The responses are from the prod environment.

Activity

  1. added 2 commits that reference this issue on Oct 19, 2026
    e619713
    3f081fe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions