Interfaces, Unions & Inputs
|
This section documents the current GraphQL specification (October 2021), plus the working draft for
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly. |
This page covers the schema-side type-system constructs used to model shapes that a flat object type cannot express on its own: interfaces and unions for polymorphic fields, input object types for structured arguments, and custom scalars for values outside GraphQL’s five built-in scalar types. It assumes the type-system vocabulary — object types, fields, arguments, the five built-in scalars — introduced in Schema and Type System.
Interfaces and the implements keyword
An interface declares a set of fields that one or more object types agree to expose, without saying which object type any particular value actually is. A field typed as an interface can then return any object type that implements it, and every client selection against that field can safely select the interface’s own fields without knowing the concrete type in advance:
interface Character {
id: ID!
name: String!
friends: [Character!]!
}
type Human implements Character {
id: ID!
name: String!
friends: [Character!]!
homePlanet: String
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character!]!
primaryFunction: String
}
Human and Droid each implement Character: both must redeclare every field the interface declares, with a
type that is identical to or a valid covariant refinement of the interface’s own field type, and both are free to
add fields of their own (homePlanet, primaryFunction) that only exist on that concrete type. A field such as
hero: Character can then resolve to either a Human or a Droid at runtime; a client selects the shared
fields directly and reaches into the type-specific ones with an inline fragment and a type condition, as covered
on Variables, Directives & Fragments. A type may
implement more than one interface, listed with & (type Human implements Character & Named), as long as it
satisfies every field each interface requires. See graphql.org’s schema guide
for the full interface rules, including interfaces that themselves implement other interfaces.
Unions and the resolve-type hook
A union is a looser form of polymorphism: it lists a set of possible object types with no shared fields at
all, not even an id. It fits a field whose possible results are genuinely unrelated shapes rather than
variations on a common concept:
union SearchResult = Human | Droid | Starship
type Query {
search(text: String!): [SearchResult!]!
}
Because a union has no fields of its own, every client selection against a union-typed field must live inside
inline or named fragments with type conditions — there is nothing to select directly, as shown in the
SearchResult example on Variables, Directives &
Fragments. On the server side, resolving a union (or an interface) field only tells the executor what data to
return; it still has to decide which member type that data actually is before it can validate the result
against the right sub-selection. That decision is made by a resolve-type hook — a function the schema
implementation supplies (commonly named resolveType or __resolveType, depending on the library) that
inspects a resolved value at runtime and returns the name of its concrete GraphQL type:
const SearchResult = new GraphQLUnionType({
name: 'SearchResult',
types: [HumanType, DroidType, StarshipType],
resolveType(value) {
if (value.primaryFunction) return 'Droid';
if (value.homePlanet) return 'Human';
return 'Starship';
},
});
The same hook exists for interfaces, for the same reason: a field typed Character still has to be resolved to
Human or Droid specifically before the executor can apply the right type-conditioned fragments and validate
the response shape. Most implementations offer a shortcut when every possible object type carries an
unambiguous discriminator already present on the resolved value (a __typename property the resolver attaches
itself, or a class/tag the library can map to a type name automatically), which removes the need to hand-write
resolveType for straightforward cases. See graphql.org’s schema guide for
the language-level union rules, and a given server library’s own documentation for how it wires up its
resolve-type hook and default discriminator behavior.
Input object types
An object type’s fields can each take scalar or enum arguments, but once a mutation needs several related values — or a query needs a structured filter — repeating a long, flat argument list on every field becomes unwieldy and impossible to reuse. An input object type groups related arguments into a single named, structured shape:
input CreateReviewInput {
episode: Episode!
stars: Int!
commentary: String
}
type Mutation {
createReview(review: CreateReviewInput!): Review!
}
An input type looks like an type, but its fields cannot take arguments of their own, and it can only be used
in input positions — as an argument type or as a field of another input type — never as the return type of a
field. That asymmetry keeps the two directions of the schema separate: type describes what the server can
return, input describes what the client can send. Nesting input types inside one another is allowed and
common for anything more structured than a handful of scalars (an address input inside an order input, for
example). See graphql.org’s schema guide for the full input-type rules,
including which default-value and nullability combinations are legal on an input field.
@oneOf input objects
An ordinary input type’s fields are all independently nullable or required — there is no built-in way to say
"exactly one of these fields must be set, and the others must be absent," which is exactly the shape needed to
model a tagged union of possible inputs (search by ID or by name, but never both; a payment method that is
either a card or a bank account). The @oneOf directive, applied to an input definition, adds that
constraint:
input SearchByInput @oneOf {
id: ID
name: String
email: String
}
type Query {
user(by: SearchByInput!): User
}
Every field of a @oneOf input is implicitly nullable in the SDL, but the executor enforces that a request
provides a value for exactly one of them — zero fields set or more than one field set is a validation
error, even though nothing in the field types themselves would otherwise forbid it. This turns what would
normally require several nullable arguments plus manual "did the caller send too many of these" checks in
resolver code into a constraint the schema itself guarantees before a resolver ever runs.
@oneOf input objects are not part of the finalized October 2021 GraphQL specification — they are defined
by the GraphQL working draft spec, alongside @defer/@stream, and support
varies by server and client library version. Check a given implementation’s own release notes before relying on
@oneOf validation being enforced automatically.
Custom scalars
GraphQL’s five built-in scalars (Int, Float, String, Boolean, ID) cover the common primitive shapes,
but a schema often needs to describe a more specific wire format — a date, a UUID, an arbitrary-precision
number, JSON itself — while still giving clients a single, self-documenting named type instead of an
unconstrained String. A custom scalar declares that name in the SDL:
scalar DateTime
type Event {
id: ID!
startsAt: DateTime!
}
The SDL declaration alone says nothing about the scalar’s actual representation; every custom scalar needs a matching server-side implementation supplying three functions that together define how a value crosses the boundary between the server’s internal representation and the wire:
| Function | Direction | Purpose |
|---|---|---|
|
Internal value → wire (response) |
Converts the resolver’s returned value (a |
|
Wire → internal value, from variables |
Converts an incoming JSON value supplied through the request’s |
|
Wire → internal value, from an inline literal |
Converts an AST literal node written directly in the query document ( |
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'An ISO-8601 encoded UTC date-time string.',
serialize(value) {
return value.toISOString(); // Date -> wire string
},
parseValue(value) {
return new Date(value); // wire string (from variables) -> Date
},
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) return null; // wire literal (from the query document) -> Date
return new Date(ast.value);
},
});
parseValue and parseLiteral end up doing equivalent work for most scalars, but they are kept distinct
because a literal’s AST node type (Kind.STRING, Kind.INT, …) has to be checked and unwrapped before its
value can even be read, while a variable arrives as an already-decoded JSON value ready to validate directly.
A schema can advertise the external specification a custom scalar follows with the @specifiedBy directive
(covered in depth on Directives), which points clients and tooling at the
formal grammar the scalar’s serialize/parseValue/parseLiteral trio actually implements.
Modelling relationships in a schema
A GraphQL schema expresses entity relationships through the shape of its fields rather than through a dedicated relationship construct:
-
One-to-one — a field typed as a single object:
type Order \{ shippingAddress: Address! }. -
One-to-many — a field typed as a list of a single object type:
type Author \{ books: [Book!]! }. -
Many-to-many — the same list-of-object shape from both sides:
type Book \{ authors: [Author!]! }alongsidetype Author \{ books: [Book!]! }, each side resolved independently by the server rather than modeled as a distinct schema construct.
A list of different types — as opposed to a list of one object type — is exactly what a union queried
through a list field achieves: search(text: String!): [SearchResult!]! above returns a single list whose
individual entries can each be a Human, a Droid, or a Starship, discriminated per item by the resolve-type
hook and read back per item with type-conditioned fragments. An interface-typed list field (characters:
[Character!]!) achieves the same heterogeneity while additionally guaranteeing a shared baseline of fields
every item exposes directly, without a fragment. Choosing between the two comes down to whether the possible
item types share a meaningful common field set (interface) or are otherwise unrelated shapes that only happen to
appear together in one list (union).
Putting it together
The SDL fragment below combines every construct from this page into one small, self-consistent schema slice: an
interface with two implementing types, a union spanning one of those types plus an unrelated one, a plain input
object, and a @oneOf input modelling a tagged union of search criteria.
interface Character {
id: ID!
name: String!
}
type Human implements Character {
id: ID!
name: String!
homePlanet: String
}
type Droid implements Character {
id: ID!
name: String!
primaryFunction: String
}
type Starship {
id: ID!
name: String!
length: Float
}
union SearchResult = Human | Droid | Starship
input CreateReviewInput {
episode: String!
stars: Int!
commentary: String
}
input CharacterFilterInput @oneOf {
id: ID
name: String
}
type Query {
characters: [Character!]!
search(text: String!): [SearchResult!]!
character(by: CharacterFilterInput!): Character
}
type Mutation {
createReview(review: CreateReviewInput!): Review!
}
characters is a homogeneous, interface-typed list — every item exposes id and name directly. search
returns a heterogeneous, union-typed list resolved per item by a resolve-type hook. character takes a
@oneOf input, so a client must supply exactly one of id or name. createReview takes a plain input object
grouping three otherwise-unrelated mutation arguments. Together they cover the polymorphic-field and
structured-argument patterns that show up repeatedly once a schema grows past its first few types; see
graphql.org’s schema guide for the type-system reference this page builds on,
and the GraphQL working draft spec for `@oneOf’s formal validation rules as
they continue to evolve.