GraphQL Schema does not match return value when using normalization groups with relations
See original GitHub issueAPI 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:
- Created 3 years ago
- Comments:7 (4 by maintainers)
Top GitHub Comments
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 🙂
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 🤔.