Skip to main content

Tagged type

At a glance

Timeline


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 and TAGGED_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.]