question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

GraphQL Schema does not match return value when using normalization groups with relations

See original GitHub issue

API Platform version(s) affected: v2.5.7, v2.6.3

Description
When using a normalization context for a GraphQL query the schema of sub resources is generated as if no normalization group was used. When doing a query the return data attributes of the sub resource are being filtered for the given group. Leading to errors like: "debugMessage": "Cannot return null for non-nullable field \"ParentEntity.testField\".".

How to reproduce
Create two entities. One entity has a ManyToMany relation to the other one and applies a normalization group:

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\ChildEntityRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(graphql={
 *      "item_query"={
 *           "normalization_context"={"groups"={"testg", "default"}},
 *      },
 *      "create"
 * })
 * @ORM\Entity(repositoryClass=ChildEntityRepository::class)
 */
class ChildEntity
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * @Groups({"testg"})
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity=ParentEntity::class)
     * @ORM\JoinColumn(nullable=false)
     * @Groups({"default"})
     */
    private $parent;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getParent(): ?ParentEntity
    {
        return $this->parent;
    }

    public function setParent(?ParentEntity $parent): self
    {
        $this->parent = $parent;

        return $this;
    }
}
<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\ParentEntityRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource()
 * @ORM\Entity(repositoryClass=ParentEntityRepository::class)
 */
class ParentEntity
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $testField;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTestField(): ?string
    {
        return $this->testField;
    }

    public function setTestField(string $testField): self
    {
        $this->testField = $testField;

        return $this;
    }
}

Add some data and run a query like:

{
  childEntity(id: "/api/child_entities/1") {
    id
    parent {
      id
      testField
    }
  }
}

Returns:

{
  "errors": [
    {
      "debugMessage": "Cannot return null for non-nullable field \"ParentEntity.testField\".",
      "message": "Internal server error",
      "extensions": {
        "category": "internal"
      },
      "locations": [
        {
          "line": 6,
          "column": 7
        }
      ],
      "path": [
        "childEntity",
        "parent",
        "testField"
      ],
      "trace": [
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 655,
          "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: String, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), null)"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 557,
          "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: String, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), null)"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 1196,
          "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: ParentEntity, array(2), instance of ArrayObject(1), array(3))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 1146,
          "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: ParentEntity, array(2), array(2), instance of ArrayObject(2))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 1107,
          "call": "GraphQL\\Executor\\ReferenceExecutor::collectAndExecuteSubfields(GraphQLType: ParentEntity, instance of ArrayObject(1), array(2), array(2))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 794,
          "call": "GraphQL\\Executor\\ReferenceExecutor::completeObjectValue(GraphQLType: ParentEntity, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), array(2))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 746,
          "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: ParentEntity, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), array(2))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 655,
          "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: ParentEntity, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), array(2))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 557,
          "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: ParentEntity, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), array(2))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 1196,
          "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: ChildEntityItem, array(4), instance of ArrayObject(1), array(2))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 1146,
          "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: ChildEntityItem, array(4), array(1), instance of ArrayObject(2))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 1107,
          "call": "GraphQL\\Executor\\ReferenceExecutor::collectAndExecuteSubfields(GraphQLType: ChildEntityItem, instance of ArrayObject(1), array(1), array(4))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 794,
          "call": "GraphQL\\Executor\\ReferenceExecutor::completeObjectValue(GraphQLType: ChildEntityItem, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(4))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 655,
          "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: ChildEntityItem, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(4))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 557,
          "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: ChildEntityItem, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(4))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 1196,
          "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: Query, null, instance of ArrayObject(1), array(1))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 264,
          "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: Query, null, array(0), instance of ArrayObject(1))"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
          "line": 215,
          "call": "GraphQL\\Executor\\ReferenceExecutor::executeOperation(instance of GraphQL\\Language\\AST\\OperationDefinitionNode, null)"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/Executor.php",
          "line": 156,
          "call": "GraphQL\\Executor\\ReferenceExecutor::doExecute()"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/GraphQL.php",
          "line": 162,
          "call": "GraphQL\\Executor\\Executor::promiseToExecute(instance of GraphQL\\Executor\\Promise\\Adapter\\SyncPromiseAdapter, instance of GraphQL\\Type\\Schema, instance of GraphQL\\Language\\AST\\DocumentNode, null, null, array(0), null, null)"
        },
        {
          "file": "/var/www/html/vendor/webonyx/graphql-php/src/GraphQL.php",
          "line": 94,
          "call": "GraphQL\\GraphQL::promiseToExecute(instance of GraphQL\\Executor\\Promise\\Adapter\\SyncPromiseAdapter, instance of GraphQL\\Type\\Schema, '{\n  childEntity(id: \"/api/child_entities/1\") {\n    id\n    parent {\n      id\n      testField\n    }\n  }\n}\n', null, null, array(0), null, null, null)"
        },
        {
          "file": "/var/www/html/vendor/api-platform/core/src/GraphQl/Executor.php",
          "line": 34,
          "call": "GraphQL\\GraphQL::executeQuery(instance of GraphQL\\Type\\Schema, '{\n  childEntity(id: \"/api/child_entities/1\") {\n    id\n    parent {\n      id\n      testField\n    }\n  }\n}\n', null, null, array(0), null, null, null)"
        },
        {
          "file": "/var/www/html/vendor/api-platform/core/src/GraphQl/Action/EntrypointAction.php",
          "line": 86,
          "call": "ApiPlatform\\Core\\GraphQl\\Executor::executeQuery(instance of GraphQL\\Type\\Schema, '{\n  childEntity(id: \"/api/child_entities/1\") {\n    id\n    parent {\n      id\n      testField\n    }\n  }\n}\n', null, null, array(0), null)"
        },
        {
          "file": "/var/www/html/vendor/symfony/http-kernel/HttpKernel.php",
          "line": 157,
          "call": "ApiPlatform\\Core\\GraphQl\\Action\\EntrypointAction::__invoke(instance of Symfony\\Component\\HttpFoundation\\Request)"
        },
        {
          "file": "/var/www/html/vendor/symfony/http-kernel/HttpKernel.php",
          "line": 79,
          "call": "Symfony\\Component\\HttpKernel\\HttpKernel::handleRaw(instance of Symfony\\Component\\HttpFoundation\\Request, 1)"
        },
        {
          "file": "/var/www/html/vendor/symfony/http-kernel/Kernel.php",
          "line": 195,
          "call": "Symfony\\Component\\HttpKernel\\HttpKernel::handle(instance of Symfony\\Component\\HttpFoundation\\Request, 1, true)"
        },
        {
          "file": "/var/www/html/public/index.php",
          "line": 20,
          "call": "Symfony\\Component\\HttpKernel\\Kernel::handle(instance of Symfony\\Component\\HttpFoundation\\Request)"
        }
      ]
    }
  ],
  "data": {
    "childEntity": null
  }
}

Possible Solution
Generate the schema based on the group for sub resources as well OR do not respect groups for sub resources. I’d be happy to implement a fix either way but not sure what would be the intended behavior.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Comments:7 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
alanpoulaincommented, Mar 9, 2021

I’m wondering if it’s already creating cache issues. Anyway, there is already different types for the result of a mutation if the normalization context is different, so we should probably do the same for nested data. You can try to open a PR if you feel motivated 🙂

0reactions
jnughcommented, Mar 9, 2021

Not sure if a schema that does not match the actual output data should have lower priority than cache efficiency. Also: If I use a different group for type A and access B trough A using that group I have to invalidate / update the cache of A anyways. So one probably has some code in place that is able to update caches according to this usecase and could simply add this case.

Another way could be to just use the default groups (e.g. from item_query or collection_query) which would be a type that is being fetched right now and therefore likely configured to be cached correctly, right?

Looking at the last example the issue could actually stay undetected because the field is actually nullable. So showing an error like You are querying a field that is not part of the current normalization group would have helped us already while debugging this 🤔.

Read more comments on GitHub >

github_iconTop Results From Across the Web

GraphQL schema basics
Your GraphQL server uses a schema to describe the shape of your available data. This schema defines a hierarchy of types with fields...
Read more >
Data normalization in GraphQL query - Stack Overflow
Is there any way to only get id fields for all users in groups and return a separate array of all unique User...
Read more >
Normalized Caching | urql Documentation
As the GraphQL API walks our query documents it may read from a relational database and entities and scalar values are copied into...
Read more >
GraphQL Support - API Platform
You can now use GraphQL at the endpoint: https://localhost:8443/graphql . ... you don't want your item to be persisted by API Platform, you...
Read more >
Five Common Problems in GraphQL Apps (And How to Fix ...
Problem: Server/Client Data Mismatch. As we've just seen, your database and GraphQL API will have different schemas, which translate into ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found