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

Fermée
#8,702 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 2 jours

Personne n'a encore pris cette issue.

Évaluation

Difficulté
3/5
Temps estimé
1-2 jours
Accessibilité débutants
68/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
php, symfony
Domaine
api

Piste de recherche

Start with JsonApi\Serializer\ItemNormalizerTrait::setAttributeValue(), which strips the data wrapper, and compare it with AbstractItemNormalizer::createConstructorArgument(), which calls createAndValidateAttributeValue() without that step. Then check denormalizeRelation() in the JSON:API serializer. Done when a constructor-argument relation accepts relationships.<name>.data for to-one (including null) and to-many, and the flat shape is rejected, with a test modelled on the Book/Author fixture.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

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.

Langage dominant
PHP
Étoiles
2.6k
Forks
988
Merge moyen
4 j 10 h
PR mergées (30 j)
109

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de api-platform/core

Toutes les issues de api-platform/core

Issues similaires

Plus d'issues PHP

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.