Tagged type
At a glance​
- Identifier: #733
- Stage: RFCX: Closed 2024-07-01T16:26:54Z
- Champion: @benjie
- PR: Tagged type
- Related:
- #586 (Input Objects accepting exactly @oneField)
- #825 (OneOf Input Objects)
- InputUnion (GraphQL Input Union)
- SchemaCoordinates (Schema Coordinates)
- wg#1071 (Struct type)
Timeline​
- Commit pushed: Separate input and output tagged types on 2021-01-21 by @benjie
- Added to 2020-10-01 WG agenda
- Mentioned in 2020-10-01 WG notes
- 9 commits pushed on 2020-09-02:
- GetTaggedMember[Field]Name by @benjie
- __[Tagged]Member[Field] by @benjie
- TAGGED_MEMBER_[FIELD_]DEFINITION by @benjie
- TaggedMember[Field]Definition/TaggedMember[Field]sDefinition by @benjie
- taggedMember[Field]Name by @benjie
- Fix definition ordering by @benjie
- Members -> member fields by @benjie
- Grammar by @benjie
- Fix incorrect capital by @benjie
- 28 commits pushed on 2020-09-01:
- Merge branch 'master' into tagged-type by @benjie
- Move TaggedMemberDefinition by @benjie
- Add TAGGED_MEMBER_DEFINITION directive location by @benjie
- Reorder so tagged types comes after interfaces/unions by @benjie
- Edit out comma that snuck in by @benjie
- Add word 'concrete' by @benjie
- Define member field by @benjie
- Add Lee's note by @benjie
- Lee's rewording by @benjie
- Add mutually exclusive tagged type example with distinct types by @benjie
- Make it clear Tagged type fields can be of any type. by @benjie
- s/objects/results by @benjie
- : by @benjie
- Checking for one key is easier than validating the given keys (maybe) by @benjie
- nit by @benjie
- Allow @deprecated on TAGGED_MEMBER_DEFINITION by @benjie
- Reword note on tagged member deprecation by @benjie
- Add note that added members must not make the tagged type invalid by @benjie
- Fix case by @benjie
- Remove duplicate by @benjie
- Reword to follow Lee's example by @benjie
- Reposition TAGGED to always be between Union and Enum. by @benjie
- Terran -> Earthling by @benjie
- Make header consistent by @benjie
- __Type represents all named types in the system by @benjie
- Apply Lee's suggestion by @benjie
- Combine by @benjie
- Use 'field' rather than 'key' by @benjie
- Added to 2020-08-06 WG agenda
- Mentioned in 2020-08-06 WG notes
- Commit pushed: Factor in review feedback from @spawnia on 2020-07-21 by @benjie
- 2 commits pushed on 2020-07-15:
- Commit pushed: Change tagged "fields" to "members" on 2020-07-03 by @benjie
- Spec PR created on 2020-06-12 by benjie
- 3 commits pushed on 2020-06-12:
- First pass by @benjie
- More edits by @benjie
- Input coercion by @benjie
THIS RFC HAS BEEN SUPERSEDED by
@oneof
, for now at least... See: https://github.com/graphql/graphql-spec/pull/825
This is an RFC for a new "Tagged type" to be added to GraphQL. It replaces the "@oneField directive" proposal following feedback from the Input Unions Working Group. Please note that "Tagged type" is the working name, and may change if we come up with a better name for it.
A Tagged type defines a list of named members each with an associated type (like the fields in Object types and Input Object types), but differs from Object types and Input Object types in that exactly one of those members must be present.
The aim of the Tagged type is to introduce a form of polymorphism in GraphQL that can be symmetric between input and output. In output, it can generally be used as an alternative to Union (the differences will be outlined below). It goes beyond interfaces and unions in that it allows the same type to be specified more than once, which is particularly useful to represent filters such as this pseudocode
{greaterThan: Int} | {lessThan: Int}
.If merged, Tagged would be the first non-leaf type kind (i.e. not a Scalar, not an Enum) that could be valid in both input and output. It is also the first kind of type where types of that kind may have different input/output suitability.
In SDL, a tagged type could look like one of these:
# suitable for input and output:
tagged StringFilter {
contains: String!
lengthAtLeast: Int!
lengthAtMost: Int!
}
# output only:
tagged Pet {
cat: Cat!
dog: Dog!
colony: ColonyType!
}
# input only:
tagged PetInput {
cat: CatInput!
dog: DogInput!
colony: ColonyType!
}(Note a number of alternative syntaxes were mooted by the Input Unions working group; the one above was chosen to be the preferred syntax.)
If we queried a
StringFilter
with the following selection set:{
contains
lengthAtLeast
lengthAtMost
}then this could yield one of the following objects:
{ "contains": "Awesome" }
{ "lengthAtLeast": 3 }
{ "lengthAtMost": 42 }
Note that each of these objects specify exactly one key.
Similarly the above JSON objects would be valid input values for the
StringFilter
where it was used as an input.Tagged vs Union for output​
Tagged does not replace Union; there are things that Union can do that tagged cannot:
{
myUnionField {
... on Node {
id # If the concrete type returned by `myUnionField` implements
# the `Node` interface, we can query `id`.
}
}
}And things that Tagged can do that Union cannot:
tagged Filter {
equalTo: Int!
lessThan: Int!
greaterThan: Int!
isNull: Boolean!
}Tagged allows for exploring the various polymorphic outputs without requiring fragments:
{
pets {
cat { name numberOfLives }
dog { name breed }
parrot { name favouritePhrase }
}
}When carefully designed and queried, the data output by a tagged output could also be usable as input to another (or the same, if it's suitable for both input and output) tagged input, giving polymorphic symmetry to your schema.
Nullability​
Tagged is designed in the way that it is so that it may leverage the existing field logic relating to nullability and errors. In particular, if you had a schema such as:
type Query {
pets: [Pet]
}
tagged Pet {
cat: Cat
dog: Dog
}
type Cat {
id: ID!
name: String!
numberOfLives: Int
}
type Dog {
id: ID!
name: String!
breed: String
}and you issued the following query:
{
pets {
cat { id name numberOfLives }
dog { id name breed }
}
}and for some reason the
name
field on Cat were to throw, the the result might come out as:{
"data": {
"pets": [
{ "cat": null },
{ "dog": { "id": "BUSTER", "name": "Buster" } }
]
},
"errors": [{ ... }]
}where we can tell an error occurred and the result would have been a
Cat
but something went wrong. This may potentially be useful, particularly for debugging, compared to returning"pets": null
or"pets": [null, {"dog": {...}}]
. It also makes implementation easier because it's the same algorithm as for object field return types.FAQ​
Can a tagged type be part of a union?​
Not as currently specified.
Can a tagged type implement an interface?​
No.
What does
__typename
return?​It returns the name of the tagged type. (This is a new behaviour, previously
__typename
would always return the name of an object type, but now we have two concrete composite output types.)What happens if I don't request the relevant tagged member?​
You'll receive an empty object. For example if you issue the selection set
{ cat }
against the tagged type below, but the result is a dog, you'll receive{}
.tagged Animal {
cat: Cat
dog: Dog
}How can I determine which field would have been returned without specifying all fields?​
There is currently no way of finding out what the field should have been other than querying every field; however there's room to solve this later with an introspection field like
__typename
(e.g.__membername
) should this show sufficient utility.Open questions​
- Should we add
isInputType
/isOutputType
to__Type
for introspection? [Author opinion: separate RFC.]- Should we use
TAGGED_INPUT
andTAGGED_OUTPUT
types separately, rather than sharing just one type? [Author opinion: no.]- Should we prevent field aliases? [Author opinion: no.]
- What exactly should the input coercion rules be, particularly around variables being omitted, e.g.
{a: $a, b: $b}
[Author opinion: as currently specified.]