Interfaces, Unions & Inputs

This section documents the current GraphQL specification (October 2021), plus the working draft for @defer/@stream/@oneOf, and the GraphQL-over-HTTP specification, as published at graphql.org and spec.graphql.org — which are the references these pages are written and verified against — and, for the integration pages, against Spring for GraphQL (2.0.x), Strawberry, Ariadne, and the client docs for Apollo Client / urql / Relay. No specific patch version is pinned. Some surfaces (Apollo Router/managed federation, graphql-ws internals, the Relay compiler internals, GraalVM native) are linked rather than documented in depth.

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

serialize

Internal value → wire (response)

Converts the resolver’s returned value (a Date object, say) into the JSON-safe value sent back to the client, and rejects values that don’t fit the scalar’s contract.

parseValue

Wire → internal value, from variables

Converts an incoming JSON value supplied through the request’s variables object into the server’s internal representation, used whenever the scalar appears as a variable-typed argument.

parseLiteral

Wire → internal value, from an inline literal

Converts an AST literal node written directly in the query document (startsAt: "2024-01-01T00:00:00Z") into the same internal representation — handled separately from parseValue because a literal arrives as a typed AST node (a string node, an int node, …​) rather than as already-parsed JSON.

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!]! } alongside type 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.