Description
Hey.
Currently, per spec, paragraph 5.3.2, two fields of the same name cannot be selected on two different, mutually exclusive types, if field types or arguments differ. E.g. given this schema:
union CatOrDog = Cat | Dog
type Query {
findPet: CatOrDog!
findDog: Dog!
}
interface Pet {
name: String!
}
type Dog implements Pet {
name: String!
nickname: String
barkVolume: Int
anotherCommonlyNamedField: String
}
type Cat implements Pet {
name: String!
nickname: String
meowVolume: Int
anotherCommonlyNamedField: String!
}
this will result in an error (Example #125 in the spec):
fragment conflictingDifferingResponses on Pet {
... on Dog {
someValue: nickname
}
... on Cat {
someValue: meowVolume
}
}
and so will this:
query {
findPet {
__typename
... on Dog {
name
anotherCommonlyNamedField
}
... on Cat {
name
anotherCommonlyNamedField
}
}
}
due to anotherCommonlyNamedField
field having different return types in Dog
and Cat
types.
It forces usage of aliases when objects are obtained through a union type. E.g. if client's uses a type, based on structure of a GraphQL type Dog
, one they can obtain from calling findDog
, they would no longer be able to have the same structure if they need to switch to findPet
.
For example, let's say a client uses this query:
{
findDog {
name
nickname
barkVolume
anotherCommonlyNamedField
}
}
and they have a type like so, used in multiple places in the project:
type Dog = {
name: string
nickname: string | null
barkVolume: number | null
anotherCommonlyNamedField: string | null
}
and then Cat
and findPet
are introduced into the schema, the client needs to support both Cat
and Dog
, they'd need to switch to the new findPet
field:
{
findPet {
__typename
... on Dog {
name
nickname
barkVolume
anotherCommonlyNamedField
}
... on Cat {
name
nickname
meowVolume
anotherCommonlyNamedField
}
}
}
which, unexpectedly, doesn't work. Instead, they have to introduce aliases:
{
findPet {
__typename
... on Dog {
name
nickname
barkVolume
anotherCommonlyNamedFieldNullable: anotherCommonlyNamedField
}
... on Cat {
name
nickname
meowVolume
anotherCommonlyNamedFieldNonNullable: anotherCommonlyNamedField
}
}
}
which breaks the original Dog
interface the client had. Now, they either have to manually convert the type to a different structure (rename anotherCommonlyNamedFieldNullable
and anotherCommonlyNamedFieldNonNullable
back into anotherCommonlyNamedField
), or change it across the entire client code.
This is suboptimal, as it forces the client to do extra work. In either situation, the client must check for the typename through __typename
- because only dogs have barkVolume
, and only cats have meowVolume
. If this check is done either way, why would different types of a shared-name field matter?
In other words, if the client already has to differentiate between different, unrelated, mutually exclusive types, that may have entirely different number of fields and an entirely different structure, why do only fields that intersect between the two have to have a shared type? If response structure is expected to be the same, regardless of the actual type of an object, then shouldn't it also enforce that a Dog
must have a meowVolume
, and a Cat
must have a barkVolume
?
Reported elsewhere
This has been reported as a bug/problem in implementations of GraphQL:
- JS: Querying same field w/ and w/o attributes for different union types results in error graphql-js#53
- PHP: OverlappingFieldsCanBeMerged throws an error when querying same field w/ and w/o attributes for different union types webonyx/graphql-php#1500
- Java: Unions too strict on field type checking? graphql-java/graphql-java#1281
- C#: Non-mergeable fields error when using UnionTypes ChilliCream/graphql-platform#2240
- ... probably others
The issues were closed as the spec dictates the specific behaviour a GraphQL server should follow.
Proposal
The proposal is to skip conflict checking for mutually exclusive types altogether. In terms of graphql-js
implementation, a patch would look like so:
- if (!areMutuallyExclusive) {
+ if (areMutuallyExclusive} {
+ return null;
+ }
An alternative proposal is to do the same, but only if __typename
field was requested.