At a glance
- Identifier: #733
- Stage: RFC X / Rejected
- Champion: @benjie
- Latest activity: 1 commit pushed on 2021-01-21
- Spec PR: https://github.com/graphql/graphql-spec/pull/733
- Related:
Spec PR description
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/isOutputTypeto__Typefor introspection? [Author opinion: separate RFC.] - Should we use
TAGGED_INPUTandTAGGED_OUTPUTtypes 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.]
Timeline
- Commit pushed on 2021-01-21 by benjie: Separate input and output tagged types
- Added to WG agenda on 2020-10-01
- Mentioned in WG notes on 2020-10-01
- 9 commits pushed on 2020-09-02:
- benjie committed "GetTaggedMember[Field]Name"
- benjie committed "__[Tagged]Member[Field]"
- benjie committed "TAGGED_MEMBER_[FIELD_]DEFINITION"
- benjie committed "TaggedMember[Field]Definition/TaggedMember[Field]sDefinition"
- benjie committed "taggedMember[Field]Name"
- benjie committed "Fix definition ordering"
- benjie committed "Members -> member fields"
- benjie committed "Grammar"
- benjie committed "Fix incorrect capital"
- 28 commits pushed on 2020-09-01:
- benjie committed "Merge branch 'master' into tagged-type"
- benjie committed "Move TaggedMemberDefinition"
- benjie committed "Add TAGGED_MEMBER_DEFINITION directive location"
- benjie committed "Reorder so tagged types comes after interfaces/unions"
- benjie committed "Edit out comma that snuck in"
- benjie committed "Add word 'concrete'"
- benjie committed "Define member field"
- benjie committed "Add Lee's note"
- benjie committed "Lee's rewording"
- benjie committed "Add mutually exclusive tagged type example with distinct types"
- benjie committed "Make it clear Tagged type fields can be of any type."
- benjie committed "s/objects/results"
- benjie committed ":"
- benjie committed "Checking for one key is easier than validating the given keys (maybe)"
- benjie committed "nit"
- benjie committed "Allow @deprecated on TAGGED_MEMBER_DEFINITION"
- benjie committed "Reword note on tagged member deprecation"
- benjie committed "Add note that added members must not make the tagged type invalid"
- benjie committed "Fix case"
- benjie committed "Remove duplicate"
- benjie committed "Reword to follow Lee's example"
- benjie committed "Reposition TAGGED to always be between Union and Enum."
- benjie committed "Terran -> Earthling"
- benjie committed "Make header consistent"
- benjie committed "__Type represents all named types in the system"
- benjie committed "Apply Lee's suggestion"
- benjie committed "Combine"
- benjie committed "Use 'field' rather than 'key'"
- Added to WG agenda on 2020-08-06
- Mentioned in WG notes on 2020-08-06
- Commit pushed on 2020-07-21 by benjie: Factor in review feedback from @spawnia
- 2 commits pushed on 2020-07-15:
- benjie committed "Merge branch 'master' into tagged-type"
- benjie committed "Sync type system"
- Commit pushed on 2020-07-03 by benjie: Change tagged "fields" to "members"
- Spec PR created on 2020-06-12 by benjie
- 3 commits pushed on 2020-06-12:
- benjie committed "First pass"
- benjie committed "More edits"
- benjie committed "Input coercion"