GraphQL Composite Schemas Spec

Introduction

The GraphQL Composite Schemas Spec introduces a comprehensive specification for creating distributed GraphQL systems that seamlessly merges multiple GraphQL schemas. This specification describes the process of composing a federated GraphQL schema and outlines algorithms for executing GraphQL queries on the federated schema effectively by using query plans. This specification was originally created by ChilliCream and was transferred to the GraphQL foundation.

The GraphQL Foundation was formed in 2019 as a neutral focal point for organizations who support the GraphQL ecosystem, and the GraphQL Specification Project was established also in 2019 as the Joint Development Foundation Projects, LLC, GraphQL Series.

If your organization benefits from GraphQL, please consider becoming a member and helping us to sustain the activities that support the health of our neutral ecosystem.

The GraphQL Specification Project has evolved and may continue to evolve in future editions of this specification. Previous editions of the GraphQL specification can be found at permalinks that match their release tag. The latest working draft release can be found at .

Conformance

A conforming implementation of the GraphQL Composite Schemas Spec must fulfill all normative requirements. Conformance requirements are described in this document via both descriptive assertions and key words with clearly defined meanings.

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative portions of this document are to be interpreted as described in IETF RFC 2119. These key words may appear in lowercase and still retain their meaning unless explicitly declared as non-normative.

A conforming implementation of the GraphQL Composite Schemas Spec may provide additional functionality, but must not where explicitly disallowed or would otherwise result in non-conformance.

Non-Normative Portions

All contents of this document are normative except portions explicitly declared as non-normative.

Examples in this document are non-normative, and are presented to aid understanding of introduced concepts and the behavior of normative portions of the specification. Examples are either introduced explicitly in prose (e.g. “for example”) or are set apart in example or counter-example blocks, like this:

Example № 1This is an example of a non-normative example.
Counter Example № 2This is an example of a non-normative counter-example.

Notes in this document are non-normative, and are presented to clarify intent, draw attention to potential edge-cases and pit-falls, and answer common questions that arise during implementation. Notes are either introduced explicitly in prose (e.g. “Note: “) or are set apart in a note block, like this:

Note This is an example of a non-normative note.

1Overview

The GraphQL Composite Schemas specification describes how to construct a single unified GraphQL schema, the composite schema, from multiple GraphQL schemas, each termed a source schema.

The composite schema presents itself as a regular GraphQL schema; the implementation details and complexities of the underlying distributed systems are not visible to clients, all observable behavior is the same as described by the GraphQL specification.

The GraphQL Composite Schemas specification has a number of design principles:

Note Although the GraphQL Composite Schemas specification does not describe how to combine arbitrary schemas, tooling may be built to transform existing or external schemas into compliant source schemas. Details of building such tooling is beyond the scope of this specification.

To enable greater interoperability between different implementations of tooling and gateways, this specification focuses on two core components: schema composition and distributed execution.

2Source Schema

A source schema is a GraphQL schema that is part of a larger composite schema. Source schemas use directives to express intent and requirements for the composition process as well as to describe runtime behavior. The following chapters describe the directives that are used to annotate a source schema.

2.1Entities and Identity

An entity is a type whose instances have an identity that is stable across source schemas, allowing the distributed GraphQL executor to recognize that data contributed by different source schemas describes the same object. An entity has one or more stable keys that represent its identity. A stable key is a set of one or more fields that represents the identity of an entity for comparison.

The identity of an entity serves two purposes: comparison and recall. Comparison recognizes that two instances refer to the same entity. Recall fetches an entity again by one of its stable keys.

The @key directive provides the declarative identity and comparison half. It declares that a type is an entity and identifies which field set or field sets are its stable keys. Declaring identity locally on the type keeps it visible without requiring a reader or tool to scan every lookup field across every source schema to infer what identifies the entity. This follows the Explicitness design principle and supports the Collaborative design principle by surfacing identity where source schemas coordinate on a shared type.

The @lookup directive provides the recall half. It lets the distributed GraphQL executor resolve an entity by a stable key in a source schema.

In the following example, the Product type declares its identity with @key(fields: "id"). The productById lookup field provides recall for the same stable key.

Example № 3type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}

type Query {
  productById(id: ID!): Product @lookup
}

A type MAY declare a @key for which no @lookup field exists in any source schema; such a stable key represents identity and supports comparison but cannot be used by the distributed GraphQL executor to resolve, or recall, the entity.

In the following example, sku is a stable key for comparison. No lookup field resolves Product by sku in any source schema.

Example № 4type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String!
}

type Query {
  productById(id: ID!): Product @lookup
}

A @key that could be inferred from a lookup field’s arguments MAY be omitted. A type that can be resolved by a stable key SHOULD declare a corresponding @key for that stable key, even though the stable key could be inferred from the lookup field’s arguments. This double bookkeeping keeps the identity of an entity explicit and locally visible on the type rather than scattered across the lookup fields of every source schema, in keeping with the Explicitness and Collaborative design principles.

Note A single identifier used both to compare and to fetch an entity couples two independent concerns. Separating the comparison stable key from the lookup mechanism lets comparison use small, stable values and avoids bloated keys and expensive comparisons.

2.2@lookup

directive @lookup on FIELD_DEFINITION

The @lookup directive is used within a source schema to specify output fields that can be used by the distributed GraphQL executor to resolve an entity by a stable key.

For a lookup field, the stable key used for recall is represented by the arguments of the field. Each lookup argument must match a field on the return type of the lookup field. The matched field does not need to be defined in the source schema that declares the lookup field; it must exist on the return type in at least one source schema. The distributed GraphQL executor resolves the key value from the source schemas where the field is available.

Source schemas can provide multiple lookup fields for the same entity to resolve the entity by different stable keys.

In this example, the source schema specifies that the Product entity can be resolved with the productById field or the productByName field. Both lookup fields are able to resolve the Product entity but do so with different stable keys.

Example № 5type Query {
  version: Int # NOT a lookup field.
  productById(id: ID!): Product @lookup
  productByName(name: String!): Product @lookup
}

type Product {
  id: ID!
  name: String!
}

Lookup fields may return object, interface, or union types. In case a lookup field returns an abstract type (interface type or union type), all possible object types of the abstract return type are considered entities, and each must have fields that correspond to every argument of the lookup field. When an argument is annotated with the @is directive, its selection map defines this correspondence instead; the selection map must cover every possible object type of the return type (see @is).

Example № 6type Query {
  product(id: ID!, categoryId: Int): Product @lookup
}

union Product = Electronics | Clothing

type Electronics {
  id: ID!
  categoryId: Int
  name: String
  brand: String
  price: Float
}

type Clothing {
  id: ID!
  categoryId: Int
  name: String
  size: String
  price: Float
}

The following example shows an invalid lookup field because the Clothing type, which is one of the possible object types of the abstract return type, does not define all the fields required by the lookup field’s arguments.

Counter Example № 7type Query {
  product(id: ID!, categoryId: Int): Product @lookup
}

union Product = Electronics | Clothing

type Electronics {
  id: ID!
  categoryId: Int
  name: String
  brand: String
  price: Float
}

# Clothing does not have a field that corresponds
# with the lookup field's argument signature.
type Clothing {
  id: ID!
  name: String
  size: String
  price: Float
}

Lookup fields must be accessible from the Query type. If a lookup field is not defined directly on the Query type, it must be reachable by following a chain of fields — starting from the Query root type — where none of the intermediate fields have arguments. This ensures that lookup fields are accessible to the executor.

Example № 8type Query {
  lookups: Lookups!
}

type Lookups {
  productById(id: ID!): Product @lookup
}

type Product {
  id: ID!
}

2.3@internal

directive @internal on OBJECT | FIELD_DEFINITION

The @internal directive is used in combination with lookup fields and allows you to declare internal types and fields. Internal types and fields do not appear in the final client-facing composite schema and do not participate in the standard schema-merging process. This allows a source schema to define lookup fields for resolving entities that should not be accessible through the client-facing composite schema.

Example № 9# Source Schema
type Query {
  productById(id: ID!): Product
  productBySku(sku: ID!): Product @internal
}

# Composite Schema
type Query {
  productById(id: ID!): Product
}

Since internal types and fields do not participate in the standard schema-merging process they do not collide with similar named fields or types on other source schemas.

Example № 10# Source Schema A
type Query {
  # this field follows the standard field merging rules
  productById(id: ID!): Product

  # this field is internal and does not follow any field merging rules.
  productBySku(sku: ID!): Product @internal
}

# Source Schema B
type Query {
  productById(id: ID!): Product
  productBySku(sku: ID!, name: String!): Product @internal
}

# Composite Schema
type Query {
  productById(id: ID!): Product
}

Internal fields can only be used by the distributed GraphQL executor as lookup fields for entity resolution.

Example № 11# Source Schema A
type Query {
  productById(id: ID!): Product @lookup
  lookups: InternalLookups! @internal
}

# all lookups within this internal type are hidden from the public API
# but can be used for entity resolution.
type InternalLookups @internal {
  productBySku(sku: ID!): Product @lookup
}

# Composite Schema
type Query {
  productById(id: ID!): Product
}

Since internal fields are not part of the standard schema-merging process, they cannot be used as key fields or in requirements. This is because there is no semantic equivalence of the field or type to another source schema.

Counter Example № 12type Query {
  productById(id: ID!): Product @lookup
}

type Product {
  id: ID! @internal
}

In contrast to @inaccessible, the effect of @internal is local to its source schema.

Example № 13# Source Schema A
type Query {
  # this field follows the standard field merging rules
  productById(id: ID!): Product

  # this field is internal and does not follow any field merging rules.
  productBySku(sku: ID!): Product @internal
}

# Source Schema B
type Query {
  # this field follows the standard field merging rules
  productById(id: ID!): Product

  # this field follows the standard field merging rules
  productBySku(sku: Int!): Product
}

# Composite Schema
type Product {
  productById(id: ID!): Product
  productBySku(sku: Int!): Product
}

2.4@inaccessible

# prettier-ignore
directive @inaccessible on
  | FIELD_DEFINITION
  | OBJECT
  | INTERFACE
  | UNION
  | ARGUMENT_DEFINITION
  | SCALAR
  | ENUM
  | ENUM_VALUE
  | INPUT_OBJECT
  | INPUT_FIELD_DEFINITION

The @inaccessible directive is used to prevent specific type system members from being accessible through the client-facing composite schema, even if they are accessible in the underlying source schemas.

This directive is useful for restricting access to type system members that are either irrelevant to the client-facing composite schema or sensitive in nature, such as internal identifiers or fields intended only for backend use.

In the following example, the key field sku is inaccessible from the composite schema. However, type system members marked as @inaccessible can still be used by the distributed executor to fulfill requirements.

Example № 14type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String! @inaccessible
  note: String
}

type Query {
  productById(id: ID!): Product
  productBySku(sku: String!): Product @inaccessible
}

In contrast to the @internal directive, @inaccessible hides type system members from the composite schema even if other source schemas on the same type system member have no @inaccessible directive.

Example № 15# Source Schema A
type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String! @inaccessible
  note: String
}

# Source Schema B
type Product @key(fields: "sku") {
  sku: String!
  price: Float!
}

# Composite Schema
type Product {
  id: ID!
  note: String
  price: Float!
}

2.5@is

directive @is(field: FieldSelectionMap!) on ARGUMENT_DEFINITION

The @is directive is utilized on lookup fields to describe how the arguments can be mapped from the entity type that the lookup field resolves. The mapping establishes semantic equivalence between disparate type system members across source schemas and is used in cases where an argument does not directly align with a field on the entity type.

An @is selection map must not supply arguments; the mapping must consist of plain field paths. The arguments of a lookup field represent a stable key of the entity, and a stable key must map to plain field values. A referenced field may still declare arguments, as long as each argument is nullable, has a default value, or is annotated with @require, so that the field can be resolved without any arguments being supplied.

In the following example, the directive specifies that the id argument on the field Query.personById and the field Person.id on the return type of the field are semantically the same.

Note In cases where the lookup argument name aligns with the field name on the return type, the @is directive can be omitted.
Example № 16type Query {
  personById(productId: ID! @is(field: "id")): Person @lookup
}

The @is directive also allows referring to nested fields relative to Person.

Example № 17type Query {
  personByAddressId(id: ID! @is(field: "address.id")): Person
}

The @is directive can be applied to multiple arguments within the same lookup field, allowing each argument to be mapped individually to fields on the return type.

Example № 18type Query {
  personByAddressId(
    id: ID! @is(field: "address.id")
    kind: PersonKind @is(field: "kind")
  ): Person
}

The @is directive can also be used in combination with @oneOf to specify a single lookup field that can resolve entities by multiple stable keys.

Example № 19type Query {
  person(
    by: PersonByInput
      @is(field: "{ id } | { addressId: address.id } | { name }")
  ): Person
}

input PersonByInput @oneOf {
  id: ID
  addressId: ID
  name: String
}

When a lookup field returns an abstract type, the selection map must cover every possible runtime type of the return type: each possible object type must be matched by at least one alternative of the selection map. When a lookup field declares multiple arguments, each argument must independently be mappable for every possible runtime type. A source schema that can only resolve a subset of the possible types must declare a narrower return type that reflects what it can resolve.

In the following example, the selection map covers all three possible types of Media, resolving each by a different key field.

Example № 20type Query {
  mediaByKey(
    key: MediaKeyInput!
      @is(
        field: "{ isbn: <Book>.isbn } | { upc: <Movie>.upc } | { feedUrl: <Podcast>.feedUrl }"
      )
  ): Media @lookup
}

input MediaKeyInput @oneOf {
  isbn: String
  upc: String
  feedUrl: String
}

interface Media {
  id: ID!
}

type Book implements Media {
  id: ID!
  isbn: String!
}

type Movie implements Media {
  id: ID!
  upc: String!
}

type Podcast implements Media {
  id: ID!
  feedUrl: String!
}

In the following counter-example, the selection map covers only Book and Movie. Podcast is a possible type of Media but is not covered by any alternative, so the lookup field is invalid.

Counter Example № 21type Query {
  mediaByKey(
    key: MediaKeyInput!
      @is(field: "{ isbn: <Book>.isbn } | { upc: <Movie>.upc }")
  ): Media @lookup
}

input MediaKeyInput @oneOf {
  isbn: String
  upc: String
}

interface Media {
  id: ID!
}

type Book implements Media {
  id: ID!
  isbn: String!
}

type Movie implements Media {
  id: ID!
  upc: String!
}

type Podcast implements Media {
  id: ID!
  feedUrl: String!
}
Arguments:
  • field: Represents a selection path map syntax.

2.6@require

directive @require(field: FieldSelectionMap!) on ARGUMENT_DEFINITION

The @require directive is used to express data requirements with other source schemas. Arguments annotated with the @require directive are removed from the composite schema and the value for these will be resolved by the distributed executor.

Example № 22type Product {
  id: ID!
  delivery(
    zip: String!
    size: Int! @require(field: "dimension.size")
    weight: Int! @require(field: "dimension.weight")
  ): DeliveryEstimates
}

The above example would translate to the following in the composite schema.

Example № 23type Product {
  id: ID!
  delivery(zip: String!): DeliveryEstimates
}

This can also be done by using input types. The selection path map specifies which data is required and needs to be resolved from other source schemas. If the input type is only used to express requirements it is removed from the composite schema.

Example № 24type Product {
  id: ID!
  delivery(
    zip: String!
    dimension: ProductDimensionInput!
      @require(field: "{ size: dimension.size, weight: dimension.weight }")
  ): DeliveryEstimates
}

If the input types do not match the output type structure the selection map syntax can be used to specify how requirements translate to the input object.

Example № 25type Product {
  id: ID!
  delivery(
    zip: String!
    dimension: ProductDimensionInput!
      @require(
        field: "{ productSize: dimension.size, productWeight: dimension.weight }"
      )
  ): DeliveryEstimates
}

type ProductDimension {
  size: Int!
  weight: Int!
}

input ProductDimensionInput {
  productSize: Int!
  productWeight: Int!
}

The @require directive can also be applied to arguments on interface fields. The selection map is rooted at the interface type and is evaluated against the concrete runtime object. The annotation must be applied consistently on the interface field and on the corresponding argument of every implementing field. The selection maps themselves may differ, and an implementing type may derive the required value from implementation-specific fields.

Example № 26interface Account {
  id: ID!
  preferredLocale: String
  displayName(locale: String @require(field: "preferredLocale")): String
}

type User implements Account {
  id: ID!
  preferredLocale: String
  displayName(locale: String @require(field: "preferredLocale")): String
}

type Organization implements Account {
  id: ID!
  preferredLocale: String
  billingLocale: String
  displayName(locale: String @require(field: "billingLocale")): String
}

Since the annotation is consistent, composition removes the locale argument from the interface field and from all implementing fields together, keeping the interface contract of the composite schema intact.

Example № 27interface Account {
  id: ID!
  preferredLocale: String
  displayName: String
}

type User implements Account {
  id: ID!
  preferredLocale: String
  displayName: String
}

type Organization implements Account {
  id: ID!
  preferredLocale: String
  billingLocale: String
  displayName: String
}

Fields referenced by a @require selection map may declare arguments. Unlike @key, @provides, and @is, which must reference plain fields, a @require selection map derives an input value and may therefore select fields with constant arguments. Argument values must be constant literals; variables are not permitted.

In the following example, the weight argument of the shippingCost field is derived from the weight field defined in another source schema, selected with the constant IMPERIAL value for the unit argument.

Example № 28# Source Schema A
type Product @key(fields: "id") {
  id: ID!
  shippingCost(
    weight: Float @require(field: "weight(unit: IMPERIAL)")
  ): Currency
}

# Source Schema B
type Product @key(fields: "id") {
  id: ID!
  weight(unit: WeightUnit!): Float
}

The @require directive must not be used on arguments of fields annotated with @lookup. The arguments of a lookup field represent the stable key with which the distributed executor resolves an entity; they are supplied from an existing representation of the entity, and a requirement has no defined meaning in that position.

Arguments:
  • field: Represents a selection path map syntax.

2.7@key

directive @key(fields: FieldSelectionSet!) repeatable on OBJECT | INTERFACE

The @key directive is used to designate a stable key of an entity, which identifies how to uniquely reference an instance of an entity across different source schemas.

Example № 29type Product @key(fields: "id") {
  id: ID!
  sku: String!
  name: String!
  price: Float!
}

Each occurrence of the @key directive on an object or interface type specifies one distinct stable key for that entity. These stable keys allow the distributed GraphQL executor to distinguish between different entities of the same type.

Example № 30type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String!
  name: String!
  price: Float!
}

While multiple stable keys define separate ways to reference the same entity based on different sets of fields, a composite stable key allows for uniquely identifying an entity by using a combination of multiple fields.

Example № 31type Product @key(fields: "id sku") {
  id: ID!
  sku: String!
  name: String!
  price: Float!
}

The directive is applicable to both OBJECT and INTERFACE types. This allows entities that implement an interface to inherit the stable keys defined at the interface level, ensuring consistent identification across different implementations of that interface.

By applying the @key directive all referenced fields become sharable even if the fields are not explicitly marked with @shareable.

Example № 32# Source Schema A
type Product @key(fields: "id") {
  id: ID!
  price: Float!
}

# Source Schema B
type Product @key(fields: "id") {
  id: ID!
  name: String!
}

Fields must be explicitly marked as part of a stable key or annotated with the @shareable directive to allow multiple source schemas to define them, ensuring that the decision to serve a field from more than one source schema is intentional and coordinated.

Counter Example № 33# Source Schema A
type Product @key(fields: "id") {
  id: ID!
  price: Float!
}

# Source Schema B
type Product {
  id: ID!
  name: String!
}
Arguments:
  • fields: Represents a field selection set syntax.

2.8@interfaceObject

directive @interfaceObject on OBJECT

The @interfaceObject directive is used within a source schema to declare an object type that acts as a stand-in for an interface defined in another source schema. The stand-in carries the same name as the interface and allows the source schema to contribute fields to the interface without defining its implementing types.

Example № 34type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

Composition merges the stand-in into the interface instead of reporting a type-kind conflict. If no source schema defines the interface, composition fails with an error.

A stand-in cannot be used as a union member or as an operation root type. Its composed type is an interface, while those positions require object types.

In the following example, source schema A defines the Media interface. Source schema B defines a stand-in for Media.

Example № 35# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  rating: Int!
}

# Composite Schema
interface Media {
  id: ID!
  title: String!
  reviews: [Review!]!
}

type Review {
  rating: Int!
}

Composition adds each stand-in field that is not part of a key to the interface and projects its implementation onto every type that implements the interface.

In the following example, composition projects the implementation of reviews from the Media stand-in onto Book, although no source schema declares the field directly on Book.

Example № 36# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  rating: Int!
}

# Composite Schema
interface Media {
  id: ID!
  title: String!
  reviews: [Review!]!
}

type Book implements Media {
  id: ID!
  title: String!
  reviews: [Review!]!
}

type Review {
  rating: Int!
}

A stand-in must declare a @key that matches one of the keys declared on the interface.

A stand-in is not required to declare a lookup field. Without one, each non-key field must either be @shareable with another effective owner reachable from every context that needs the field, or be replaced with @implement on every implementing type. A stand-in that declares only key fields serves as a typed reference to the entity.

Example № 37# Source Schema A
type Media @interfaceObject @key(fields: "id") {
  id: ID!
}

type Rating {
  id: ID!
  subject: Media!
  stars: Int!
}

# Source Schema B
type Query {
  mediaById(id: ID!): Media @lookup
}

interface Media @key(fields: "id") {
  id: ID!
}

type Book implements Media {
  id: ID!
}

2.9@shareable

directive @shareable repeatable on OBJECT | FIELD_DEFINITION

By default, only a single source schema is allowed to contribute a particular field to an object type. This prevents source schemas from inadvertently defining similarly named fields that are not semantically equivalent.

Counter Example № 38# Schema A
type Product {
  name: String!
  description: String!
}

# Schema B
type Product {
  name: String!
  variation: ProductVariation!
}

Fields must be explicitly marked as @shareable to allow multiple source schemas to define them, ensuring that the decision to serve a field from more than one source schema is intentional and coordinated.

Example № 39# Schema A
type Product {
  name: String! @shareable
  description: String!
}

# Schema B
type Product {
  name: String! @shareable
  variation: ProductVariation!
}

If multiple source schemas define the same sharable field, they are assumed to be semantically equivalent, and the executor is free to choose between them as it sees fit.

This also applies when a source schema defines both a stand-in and concrete types that implement the corresponding interface in the composite schema. When direct declarations and projected implementations overlap, all eligible declarations must be shareable and satisfy the field compatibility rules. The same applies to implementations projected from multiple interfaces. A concrete type or a more-specific interface does not take precedence over other implementations. These rules also apply when the declarations are in the same source schema.

In the following example, source schema B provides a projected implementation of taxRate, while source schema C declares the same field directly on Chair. Both declarations are @shareable, so both remain eligible for Chair.taxRate.

Example № 40# Source Schema A
interface Product @key(fields: "id") {
  id: ID!
  name: String!
}

type Chair implements Product @key(fields: "id") {
  id: ID!
  name: String!
}

type Table implements Product @key(fields: "id") {
  id: ID!
  name: String!
}

# Source Schema B
type Product @interfaceObject @key(fields: "id") {
  id: ID!
  taxRate: Float @shareable
}

# Source Schema C
type Chair @key(fields: "id") {
  id: ID!
  taxRate: Float @shareable
}

The executor may resolve Chair.taxRate through source schema B or source schema C. Table has no direct declaration, so source schema B remains its only eligible implementation of taxRate.

The @shareable directive can also be applied at the object-type level, having the same effect as if @shareable were applied to each field of the type.

Example № 41# Schema A
type Product @shareable {
  name: String!
  description: String!
}

# Schema B
type Product {
  name: String! @shareable
  variation: ProductVariation!
}

Key fields of an object-type are considered shareable by default and do not need to be explicitly marked with @shareable.

Example № 42# Schema A
type Product @key(fields: "id") {
  id: ID!
  name: String! @shareable
  description: String!
}

# Schema B
type Product @key(fields: "id") {
  id: ID!
  name: String! @shareable
  variation: ProductVariation!
}

2.10@provides

directive @provides(fields: FieldSelectionSet!) on FIELD_DEFINITION

The @provides directive indicates that a field can provide certain subfields of its return type from the same source schema, without requiring an additional resolution step elsewhere.

Example № 43type Review {
  id: ID!
  body: String!
  author: User @provides(fields: "email")
}

type User @key(fields: "id") {
  id: ID!
  email: String! @external
  name: String!
}

type Query {
  reviews: [Review!]
  users: [User!]
}

When a field annotated with @provides returns an object, interface or union type that may also be contributed by other source schemas, this directive declares which of that type’s subfields the current source schema can resolve directly.

Example № 44{
  reviews {
    body
    author {
      name
      email
    }
  }
}

If a client tries to fetch the same subfield (User.email) through a different path (e.g., users query field), the source schema will not be able to resolve it and will throw an error.

Counter Example № 45{
  users {
    # The source schema does NOT provide email in this context,
    # and this field will fail at runtime.
    email
  }
}

The @provides directive may reference multiple fields or nested fields:

Example № 46type Review {
  id: ID!
  product: Product @provides(fields: "sku variation { size }")
}

type Product @key(fields: "sku variation { id }") {
  sku: String! @external
  variation: ProductVariation!
  name: String!
}

type ProductVariation {
  id: String!
  size: String! @external
}

When a field annotated with the provides directive has an abstract return type the fields syntax can leverage inline fragments to express fields that can be resolved locally.

Example № 47type Review {
  id: ID!
  # The @provides directive tells us that this source schema can supply different
  # fields depending on which concrete type of Product is returned.
  product: Product
    @provides(
      fields: """
      ... on Book { author }
      ... on Clothing { size }
      """
    )
}

interface Product @key(fields: "id") {
  id: ID!
}

type Book implements Product {
  id: ID!
  title: String!
  author: String! @external
}

type Clothing implements Product {
  id: ID!
  name: String!
  size: String! @external
}

type Query {
  reviews: [Review!]!
}

The @provides directive is an execution-time optimization and never a requirement for resolvability. Composition validates that every query path of the composite schema remains satisfiable with all @provides directives ignored. A @provides directive allows the distributed GraphQL executor to obtain the selected fields in the same response and thereby reduce the number of source schema requests, but the selected fields must remain resolvable without it.

Arguments:
  • fields: Represents a field selection set syntax describing the subfields of the returned type that can be provided by the current source schema.

2.11@external

directive @external on FIELD_DEFINITION

The @external directive indicates that a field is recognized by the current source schema but is not directly contributed (resolved) by it. Instead, this schema references the field for specific composition purposes.

Stable Keys

When combined with one or more @key directives, an external field can serve as a stable key (or part of a composite stable key).

Example № 48type Query {
  productBySku(sku: String!): Product @lookup
  productByUpc(upc: String!): Product @lookup
}

type Product @key(fields: "sku") @key(fields: "upc") {
  sku: String! @external
  upc: String! @external
  name: String
}
Field Resolution

When another field in the same source schema uses @provides to declare that it can resolve certain external fields in a single data-fetching step.

Example № 49type Review {
  id: ID!
  text: String
  author: User @provides(fields: "email")
}

type User {
  id: ID!
  email: String! @external
}

When a field is marked @external, the composition process understands that the field is provided by another source schema. The current source schema references it only for entity identification (via @key) or for providing a field through @provides. If no such usage exists, the presence of an @external field produces a composition error.

The distributed GraphQL executor never requests a field marked @external from the declaring source schema directly. The field is resolved either by a source schema that defines it without @external, or – when reached through a field annotated with @provides - as part of the providing source schema’s response. The value of an external key field may also be known to the executor without resolving the field, for example, when it was used as the input of a lookup field that resolved the entity.

2.12@override

directive @override(from: String!) on FIELD_DEFINITION

The @override directive is used to migrate a field from one source schema to another. When a field in the local schema is annotated with @override(from: "Catalog"), it signals that the local schema overrides the field previously contributed by the Catalog source schema. As a result, the composite schema will source this field from the local schema, rather than from the original source schema.

The following example shows how a field can be migrated from the Catalog schema to the new Payments schema. By using @override, a field can be moved to a new schema without requiring any change to the original Catalog schema.

Example № 50# The original "Catalog" schema:
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}

# The new "Payments" schema:
type Product @key(fields: "id") {
  id: ID! @external
  price: Float! @override(from: "Catalog")
  tax: Float!
}

A field annotated with @override must not be the target of another @override. The following override chain is invalid because the declaration in Payments is annotated with @override.

Counter Example № 51# The original "Catalog" schema:
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}

# The new "Payments" schema:
type Product @key(fields: "id") {
  id: ID! @external
  price: Float! @override(from: "Catalog")
  tax: Float!
}

# The new "Pricing" schema:
type Product @key(fields: "id") {
  id: ID! @external
  price: Float! @override(from: "Payments")
}

Composition also rejects cyclic overrides and multiple overrides that target the same declaration. The following counter-example has a cycle between Catalog and Payments.

Counter Example № 52# The original "Catalog" schema:
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float! @override(from: "Payments")
}

# The new "Payments" schema:
type Product @key(fields: "id") {
  id: ID! @external
  price: Float! @override(from: "Catalog")
  tax: Float!
}

The @override directive may also be applied to a field on an @interfaceObject stand-in. It overrides declarations of that field in the source schema named by from that contribute to the interface and its implementations. This includes the matching stand-in field, allowing a projected implementation to move from one source schema to another.

A field projected from a stand-in is not itself a declaration for the purposes of @override. An @override on an implementing type therefore replaces only a direct declaration on that type in the named source schema. If a direct declaration and a projected implementation both apply, they must satisfy the @shareable and field-compatibility rules.

In the following example, the Catalog schema originally declares reviews on the Media interface and implements it directly on Book and Movie. The Reviews schema takes over by declaring Media as a stand-in and overriding the field from Catalog.

Example № 53# The original "Catalog" schema:
interface Media @key(fields: "id") {
  id: ID!
  title: String!
  reviews: [Review!]!
}

type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
  author: String!
  reviews: [Review!]!
}

type Movie implements Media @key(fields: "id") {
  id: ID!
  title: String!
  director: String!
  reviews: [Review!]!
}

type Review {
  id: ID! @shareable
  rating: Int! @shareable
}

# The new "Reviews" schema:
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]! @override(from: "Catalog")
}

type Review {
  id: ID! @shareable
  rating: Int! @shareable
}

Composition drops Book.reviews and Movie.reviews from the Catalog schema. It instead projects reviews onto Media, and from there onto Book and Movie, using the implementation contributed by the Reviews stand-in. The composite schema is unchanged; only the source of the field moves.

Arguments:
  • from: The name of the source schema that originally provided this field.

3Schema Composition

The schema composition describes the process of merging multiple source schemas into a single GraphQL schema, known as the composite execution schema, which is a valid GraphQL schema annotated with execution directives. This composite execution schema is the output of the schema composition process. The schema composition process is divided into three main steps: Validate Source Schemas, Merge Source Schemas, and Validate Satisfiability, which are run in sequence to produce the composite execution schema.

Although this chapter describes schema composition as a sequence of phases, an implementation is not required to implement these steps exactly as presented. Implementations may interleave or reorder the specified checks, or introduce additional processing stages, provided that the final composed schema complies with the requirements set forth in this specification. The composition rules and resulting schema must remain consistent, but the specific structure or timing of each validation step is left to the implementer.

3.1Validate Source Schemas

In this phase, each source schema is validated in isolation to ensure that it satisfies the GraphQL specification and composition requirements. No cross-schema references are considered here. Each source schema must have valid syntax, well-formed type definitions, and correct directive usage. If any source schema fails these checks, composition does not proceed.

3.1.1Validate Type System

3.1.1.1Invalid GraphQL

Error Code

INVALID_GRAPHQL

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • schema must be a syntactically valid
  • schema must be a semantically valid GraphQL schema according to the GraphQL specification.
Explanatory Text

Before composition, every individual source schema must be valid as per the official GraphQL specification. Common reasons a schema may be considered “invalid GraphQL” include:

  • Syntax Errors: Missing braces, invalid tokens, or misplaced punctuation.
  • Unknown Types: Referencing types that are not defined within the schema or imported from elsewhere.
  • Invalid Directive Usage: Omitting required arguments to directives or using directives in disallowed locations.
  • Invalid Default Values: Providing default values for arguments or fields that do not conform to the type (e.g., a default of null for a non-null field, an invalid enum value, etc.).
  • Conflicting Type Definitions: Defining or overriding a built-in type or directive incorrectly.

When any of these validation checks fail for a particular source schema, that schema does not meet the baseline requirements for composition, and the composition process cannot proceed. An INVALID_GRAPHQL error is raised, prompting the schema owner to correct the GraphQL violations before retrying composition.

Examples

In the following counter-example, the schema is invalid because the type User is referenced in the Query type but never defined:

Counter Example № 54type Query {
  user: User
}

# The type "User" is never defined; this is invalid GraphQL.

In this counter-example, "INVALID_VALUE" is not a valid Role, causing INVALID_GRAPHQL.

Counter Example № 55enum Role {
  ADMIN
  USER
}

type Query {
  users(role: Role = "INVALID_VALUE"): [String]
}

The GraphQL spec requires all non-null directive arguments to be supplied. The omission of the fields argument in the @provides directive triggers INVALID_GRAPHQL.

Counter Example № 56directive @provides(fields: String!) on FIELD_DEFINITION

type Product {
  price: Float @provides
  # "fields" argument is required, but not provided.
}

3.1.1.2Disallowed Inaccessible Elements

Error Code

DISALLOWED_INACCESSIBLE

Severity

ERROR

Formal Specification
  • Let type be the set of all types the schema.
  • For each type in types:
    • If type is a built-in scalar type or introspection type:
      • IsAccessible(type) must be true.
      • For each field in type:
        • IsAccessible(field) must be true.
        • For each argument in field:
          • IsAccessible(argument) must be true.
  • For each directive in directives:
    • If directive is a built-in directive:
      • For each argument in directive:
        • IsAccessible(argument) must be true.
Explanatory Text

This rule ensures that certain essential elements of a GraphQL schema, particularly built-in scalars, directive arguments, and introspection types, cannot be marked as @inaccessible. These types are fundamental to GraphQL. Making these elements inaccessible would break core GraphQL functionality.

Here, the String type is not marked as @inaccessible, which adheres to the rule:

Example № 57type Product {
  price: Float
  name: String
}

In this example, the String scalar is marked as @inaccessible. This violates the rule because String is a required built-in type that cannot be inaccessible:

Counter Example № 58scalar String @inaccessible

type Product {
  price: Float
  name: String
}

In this example, the introspection type __Type is marked as @inaccessible. This violates the rule because introspection types must remain accessible for GraphQL introspection queries to work.

Counter Example № 59type __Type @inaccessible {
  kind: __TypeKind!
  name: String
  fields(includeDeprecated: Boolean! = false): [__Field!]
}

3.1.1.3Type Definition Invalid

Error Code

TYPE_DEFINITION_INVALID

Severity

ERROR

Formal Specification
  • Let types be the set of built-in types (for example, FieldSelectionMap) defined by the composition specification from the schema.
  • For each type in types:
    • Let kind be the kind of type.
    • kind must be equal to the kind defined by the composition specification.
    • If type is a directive:
      • Let expectedArguments be the set of arguments defined by the composition specification.
      • For each expectedArgument in expectedArguments:
        • Let name be the name of expectedArgument.
        • Let argument be the argument with name in type.
        • argument must be defined.
        • Let expectedType be the type of expectedArgument.
        • Let type be the type of argument.
        • type must be equal to expectedType.
Explanatory Text

Certain types (and directives) are reserved in composite schema specification for specific purposes and must adhere to the specification’s definitions. For example, FieldSelectionMap is a built-in scalar that represents a selection of fields as a string. Redefining these built-in types with a different kind (e.g., an input object, enum, union, or object type) is disallowed and makes the composition invalid.

To ensure schema evolution and interoperability, directives may include additional arguments, provided that all required arguments defined by the specification are present.

This rule ensures that built-in types maintain their expected shapes and semantics so the composed schema can correctly interpret them.

Examples

In the following counter-example, FieldSelectionMap is declared as an input type instead of the required scalar. This leads to a TYPE_DEFINITION_INVALID error because the defined scalar FieldSelectionMap is being overridden by an incompatible definition.

Counter Example № 60directive @require(field: FieldSelectionMap!) on ARGUMENT_DEFINITION

input FieldSelectionMap {
  fields: [String!]!
}

In the following example, the @key directive includes an additional argument, futureArg, which is not part of the specification. This is valid and allows the directive to evolve without breaking existing schemas.

Example № 61directive @key(
  fields: FieldSelectionSet!
  futureArg: String
) repeatable on OBJECT | INTERFACE

However, if the @key directive is defined without the required fields argument, as shown below, it results in a TYPE_DEFINITION_INVALID error.

Counter Example № 62directive @key(futureArg: String) repeatable on OBJECT | INTERFACE

3.1.1.4Query Root Type Inaccessible

Error Code

QUERY_ROOT_TYPE_INACCESSIBLE

Severity

ERROR

Formal Specification
  • Let queryType be the query operation type defined in the schema.
  • If queryType is annotated with @inaccessible:
    • Produce a QUERY_ROOT_TYPE_INACCESSIBLE error.
Explanatory Text

Every source schema that contributes to the final composite schema must expose a public (accessible) root query type. Marking the root query type as @inaccessible makes it invisible to the gateway, defeating its purpose as the primary entry point for queries and lookups.

Examples

In this example, no @inaccessible annotation is applied to the query root, so the rule is satisfied.

Example № 63schema {
  query: Query
}

type Query {
  allBooks: [Book]
}

type Book {
  id: ID!
  title: String
}

Since the schema marks the query root type as @inaccessible, the rule is violated. QUERY_ROOT_TYPE_INACCESSIBLE is raised because a schema’s root query type cannot be hidden from consumers.

Counter Example № 64schema {
  query: Query
}

type Query @inaccessible {
  allBooks: [Book]
}

type Book {
  id: ID!
  title: String
}

3.1.1.5Root Mutation Used

Error Code

ROOT_MUTATION_USED

Severity

ERROR

Formal Specification
  • Let rootMutation be the root mutation type defined in the schema, if it exists.
  • Let namedMutationType be the type with the name Mutation in schema, if it exists.
  • If rootMutation is defined:
    • rootMutation must be named Mutation.
  • Otherwise, namedMutationType must not be defined.
Explanatory Text

This rule enforces that, for any source schema, if a root mutation type is defined, it must be named Mutation. Defining a root mutation type with a name other than Mutation or using a differently named type alongside a type explicitly named Mutation creates inconsistencies in schema design and violates the composite schema specification.

Examples

Valid example:

Example № 65schema {
  mutation: Mutation
}

type Mutation {
  createProduct(name: String): Product
}

type Product {
  id: ID!
  name: String
}

The following counter-example violates the rule because RootMutation is used as the root mutation type, but a type named Mutation is also defined.

Counter Example № 66schema {
  mutation: RootMutation
}

type RootMutation {
  createProduct(name: String): Product
}

type Mutation {
  deprecatedField: String
}

3.1.1.6Root Query Used

Error Code

ROOT_QUERY_USED

Severity

ERROR

Formal Specification
  • Let rootQuery be the root query type defined in the schema, if it exists.
  • Let namedQueryType be the type with the name Query in schema, if it exists.
  • If rootQuery is defined:
    • rootQuery must be named Query.
  • Otherwise, namedQueryType must not be defined.
Explanatory Text

This rule enforces that the root query type in any source schema must be named Query. Defining a root query type with a name other than Query or using a differently named type alongside a type explicitly named Query creates inconsistencies in schema design and violates the composite schema specification.

Examples

Valid example:

Example № 67schema {
  query: Query
}

type Query {
  product(id: ID!): Product
}

type Product {
  id: ID!
  name: String
}

The following counter-example violates the rule because RootQuery is used as the root query type, but a type named Query is also defined.

Counter Example № 68schema {
  query: RootQuery
}

type RootQuery {
  product(id: ID!): Product
}

type Query {
  deprecatedField: String
}

3.1.1.7Root Subscription Used

Error Code

ROOT_SUBSCRIPTION_USED

Severity

ERROR

Formal Specification
  • Let rootSubscription be the root mutation type defined in the schema, if it exists.
  • Let namedSubscriptionType be the type with the name Subscription in schema, if it exists.
  • If rootSubscription is defined:
    • rootSubscription must be named Subscription.
  • Otherwise, namedSubscriptionType must not be defined.
Explanatory Text

This rule enforces that, for any source schema, if a root subscription type is defined, it must be named Subscription. Defining a root subscription type with a name other than Subscription or using a differently named type alongside a type explicitly named Subscription creates inconsistencies in schema design and violates the composite schema specification.

Examples

Valid example:

Example № 69schema {
  subscription: Subscription
}

type Subscription {
  productCreated: Product
}

type Product {
  id: ID!
  name: String
}

The following counter-example violates the rule because RootSubscription is used as the root subscription type, but a type named Subscription is also defined.

Counter Example № 70schema {
  subscription: RootSubscription
}

type RootSubscription {
  productCreated: Product
}

type Subscription {
  deprecatedField: String
}

3.1.2Validate Internal Directives

3.1.2.1Internal Override Collision

Error Code

INTERNAL_OVERRIDE_COLLISION

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the object types in schema.
  • For each type in types:
    • If type is annotated with @internal:
      • No field on type may be annotated with @override.
    • For each field on type:
      • If field is annotated with @internal:
        • field must not be annotated with @override.
Explanatory Text

An @internal declaration does not participate in composition, while an @override declaration transfers a composed field from another source schema. The directives are therefore mutually exclusive.

3.1.3Validate External Directives

3.1.3.1External Unused

Error Code

EXTERNAL_UNUSED

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all composite types (object, interface) in schema.
  • For each type in types:
    • Let fields be the set of fields for type.
    • For each field in fields:
      • If field is marked with @external:
        • Let keyReferences be the set of @key directives on types in schema whose fields selection selects field, including through nested selections.
        • Let providesReferences be the set of @provides directives on fields in schema whose fields selection selects field, including through nested selections.
        • The union of keyReferences and providesReferences must not be empty.
Explanatory Text

A field marked with @external is not resolved by the declaring source schema; it is declared so that the source schema can reference it for composition purposes. There are exactly two such purposes: entity identification, where the field is selected by a @key directive, and field provision, where the field is selected by a @provides directive. An @external field that is referenced by neither @key nor @provides serves no purpose and is likely a leftover from an incomplete refactoring; this rule reports it as an error.

Note @require and @is express requirements through a FieldSelectionMap that is resolved against data provided by other source schemas; they do not rely on a local @external field declaration. References within @require or @is therefore do not count as usage of an @external field.
Examples

In this example, the name field is marked with @external and is referenced by the @provides directive, satisfying the rule:

Example № 71# Source Schema A
type Product {
  id: ID
  name: String @external
}

type Query {
  productByName(name: String): Product @provides(fields: "name")
}

In this example, the sku and upc fields are marked with @external and are each referenced by a @key directive on their declaring type, satisfying the rule:

Example № 72# Source schema A
type Product @key(fields: "sku") @key(fields: "upc") {
  sku: String! @external
  upc: String! @external
  name: String
}

type Query {
  productBySku(sku: String!): Product @lookup
  productByUpc(upc: String!): Product @lookup
}

In this example, the name field is marked with @external but is referenced by neither a @key directive nor a @provides directive, violating the rule:

Counter Example № 73# Source Schema A
type Product {
  id: ID
  name: String @external
}

3.1.3.2External Override Collision

Error Code

EXTERNAL_OVERRIDE_COLLISION

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all INTERFACE and OBJECT types in schema.
  • For each type in types:
    • Let fields be the set of fields on type.
    • For each field in fields:
      • If field is annotated with @external:
        • field must not be annotated with @override
Explanatory Text

The @external directive indicates that a field is defined in a different source schema, and the current schema merely references it. Therefore, a field marked with @external must not simultaneously carry directives that assume local ownership or resolution responsibility, such as @override, which transfers ownership of the field’s definition from one schema to another, and is incompatible with an already-external field definition.

Examples

In this scenario, User.fullName is defined in Schema A but overridden in Schema B. Since @override is not combined with @external on the same field, no collision occurs.

Example № 74# Source Schema A
type User {
  id: ID!
  fullName: String
}

# Source Schema B
type User {
  id: ID!
  fullName: String @override(from: "SchemaA")
}

Here, amount is marked with both @override and @external. This violates the rule because the field is simultaneously labeled as “override from another schema” and “external” in the local schema, producing an EXTERNAL_OVERRIDE_COLLISION error.

Counter Example № 75# Source Schema A
type Payment {
  id: ID!
  amount: Int
}

# Source Schema B
type Payment {
  id: ID!
  amount: Int @override(from: "SchemaA") @external
}

3.1.3.3External Provides Collision

Error Code

EXTERNAL_PROVIDES_COLLISION

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all INTERFACE and OBJECT types in schema.
  • For each type in types:
    • Let fields be the set of fields on type.
    • For each field in fields:
      • If field is annotated with @external:
        • field must not be annotated with @provides
Explanatory Text

The @external directive indicates that a field is defined in a different source schema, and the current schema merely references it. Therefore, a field marked with @external must not simultaneously carry directives that assume local ownership or resolution responsibility, such as @provides, which declares that the field can supply additional nested fields from the local schema, conflicting with the notion of an external field whose definition resides elsewhere.

Examples

In this example, description is only annotated with @provides in Schema B, without any other directive. This usage is valid.

Example № 76# Source Schema A
type Invoice {
  id: ID!
  description: String
}

# Source Schema B
type Invoice {
  id: ID!
  description: String @provides(fields: "length")
}

In this counter-example, description is annotated with @external and also with @provides. Because @external and @provides cannot co-exist on the same field, an EXTERNAL_PROVIDES_COLLISION error is produced.

Counter Example № 77# Source Schema A
type Invoice {
  id: ID!
  description: String
}

# Source Schema B
type Invoice {
  id: ID!
  description: String @external @provides(fields: "length")
}

3.1.3.4External Require Collision

Error Code

EXTERNAL_REQUIRE_COLLISION

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all INTERFACE and OBJECT types in schema.
  • For each type in types:
    • Let fields be the set of fields on type.
    • For each field in fields:
      • If field is annotated with @external:
        • For each argument in field:
          • argument must not be annotated with @require
Explanatory Text

The @external directive indicates that a field is defined in a different source schema, and the current schema merely references it. Therefore, a field marked with @external must not simultaneously carry directives that assume local ownership or resolution responsibility, such as @require, which specifies dependencies on other fields to resolve this field. Since @external fields are not locally resolved, there is no need for @require.

Examples

In this example, title has arguments annotated with @require in Schema B, but is not marked as @external. This usage is valid.

Example № 78# Source Schema A
type Book {
  id: ID!
  title: String
  subtitle: String
}

# Source Schema B
type Book {
  id: ID!
  title(subtitle: String @require(field: "subtitle")): String
}

The following example is invalid, since title is marked with @external and has an argument that is annotated with @require. This conflict leads to an EXTERNAL_REQUIRE_COLLISION error.

Counter Example № 79# Source Schema A
type Book {
  id: ID!
  title: String
  subtitle: String
}

# Source Schema B
type Book {
  id: ID!
  title(subtitle: String @require(field: "subtitle")): String @external
}

3.1.3.5External on Interface

Error Code

EXTERNAL_ON_INTERFACE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all composite types in schema.
  • For each type in types:
    • If type is an interface type:
      • Let fields be the set of fields on type.
      • For each field in fields:
        • field must not be annotated with @external
Explanatory Text

The @external directive indicates that a field is defined and resolved elsewhere, not in the current schema. In the case of an interface type, fields are abstract - they do not have direct resolutions at the interface level. Instead, each implementing object type provides the concrete field implementations. Marking an interface field with @external is therefore nonsensical, as there is no actual field resolution in the interface itself to “borrow” from another schema. Such usage raises an EXTERNAL_ON_INTERFACE error.

Examples

Here, the interface Node merely describes the field id. Object types User and Product implement and resolve id. No @external usage occurs on the interface itself, so no error is triggered.

Example № 80interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  name: String
}

type Product implements Node {
  id: ID!
  price: Int
}

Since id is declared on an interface and marked with @external, the composition fails with EXTERNAL_ON_INTERFACE. An interface does not own the concrete field resolution, so it is invalid to mark any of its fields as external.

Counter Example № 81interface Node {
  id: ID! @external
}

3.1.4Validate `@is` Directive

3.1.4.1Is Invalid Syntax

Error Code

IS_INVALID_SYNTAX

Severity

ERROR

Formal Specification
  • Let types be the set of all INTERFACE and OBJECT types in the source schema.
  • For each type in types:
    • Let fields be the set of all lookup fields on type.
    • Let arguments be the set of all arguments on fields.
    • For each argument in arguments:
      • If argument is annotated with @is:
        • Let fieldArg be the string value of the field argument of the @is directive on argument.
        • fieldArg must be be parsable as a valid FieldSelectionMap.
Explanatory Text

The @is directive’s field argument must be syntactically valid GraphQL. If the FieldSelectionMap string is malformed (e.g., missing closing braces, unbalanced quotes, invalid tokens), then the schema cannot be composed correctly. In such cases, the error IS_INVALID_SYNTAX is raised.

Examples

In the following example, the @is directive’s field argument is a valid FieldSelectionMap and satisfies the rule.

Example № 82type Query {
  product(id: ID! @is(field: "id")): Product @lookup
}

type Product {
  id: ID!
  name: String
}

In the following counter-example, the @is directive’s field argument has invalid syntax because it is missing a closing brace.

Counter Example № 83type Query {
  product(id: ID! @is(field: "{ id ")): Product @lookup
}

type Product {
  id: ID!
  name: String
}

3.1.4.2Is Invalid Field Type

Error Code

IS_INVALID_FIELD_TYPE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let compositeTypes be the set of all composite types in schema.
  • For each composite in compositeTypes:
    • Let fields be the set of fields on composite.
    • Let arguments be the set of all arguments on fields.
    • For each argument in arguments:
      • If argument is not annotated with @is:
        • Continue
      • Let fieldArg be the value of the field argument of the @is directive on argument.
      • If fieldArg is not a string:
        • Produce an IS_INVALID_FIELD_TYPE error.
Explanatory Text

When using the @is directive, the field argument must always be a string that describes how the arguments can be mapped from the entity type that the lookup field resolves. If the field argument is provided as a type other than a string (such as an integer, boolean, or enum), the directive usage is invalid and will cause schema composition to fail.

Examples

In the following example, the @is directive’s field argument is a valid string and satisfies the rule.

Example № 84type Query {
  personById(id: ID! @is(field: "id")): Person @lookup
}

type Person {
  id: ID!
  name: String
}

Since field is set to 123 (an integer) instead of a string, this violates the rule and triggers an IS_INVALID_FIELD_TYPE error.

Counter Example № 85type Query {
  personById(id: ID! @is(field: 123)): Person @lookup
}

type Person {
  id: ID!
  name: String
}

3.1.4.3Is Invalid Usage

Error Code

IS_INVALID_USAGE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let compositeTypes be the set of all composite types in schema.
  • For each compositeType in compositeTypes:
    • Let fields be the set of fields on compositeType.
    • For each field in fields:
      • Let arguments be the set of all arguments on field.
      • For each argument in arguments:
        • If argument is not annotated with @is:
          • Continue
        • field must be annotated with @lookup
Explanatory Text

When using the @is directive, the field declaring the argument must be a lookup field (i.e. have the @lookup directive applied).

Examples

In the following example, the @is directive is applied to an argument declared on a field with the @lookup directive, satisfying the rule.

Example № 86type Query {
  personById(id: ID! @is(field: "id")): Person @lookup
}

type Person {
  id: ID!
  name: String
}

In the following counter-example, the @is directive is applied to an argument declared on a field without the @lookup directive, violating the rule.

Counter Example № 87type Query {
  personById(id: ID! @is(field: "id")): Person
}

type Person {
  id: ID!
  name: String
}

3.1.4.4Is Fields Has Arguments

Error Code

IS_FIELDS_HAS_ARGUMENTS

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let compositeTypes be the set of all composite types in schema.
  • For each compositeType in compositeTypes:
    • Let fields be the set of fields on compositeType.
    • For each field in fields:
      • Let arguments be the set of arguments on field.
      • For each argument in arguments:
        • If argument is not annotated with @is:
          • Continue
        • Let selectionMap be the parsed selection map of the field argument of the @is directive on argument.
        • Each selection in selectionMap, including nested selections and path segments, must not supply arguments.
Explanatory Text

The arguments of a lookup field represent the stable key with which the distributed GraphQL executor recalls an entity, and the @is directive maps each argument to a field of the entity. Such a mapping must consist of plain field paths; supplying arguments within an @is selection map is not allowed.

A referenced field may still declare arguments, as long as each argument is nullable, has a default value, or is annotated with @require and therefore supplied by the executor. Such a field can be resolved without any arguments being supplied. A field that requires an argument cannot be referenced by an @is selection map, since the map cannot supply one (see Is Invalid Fields and the argument validation rules in Appendix A).

The same applies to @key (see Key Fields Has Arguments). @provides selections must reference fields that declare no arguments other than @require-annotated ones (see Provides Fields Has Arguments). Only @require selection maps may supply constant arguments, as they derive input values rather than keys.

Examples

In this example, the id argument of the lookup field is mapped to the plain id field of Product, satisfying the rule.

Example № 88type Query {
  productById(id: ID! @is(field: "id")): Product @lookup
}

type Product {
  id: ID!
  name: String
}

In this example, the referenced field id declares the nullable scope argument. Since scope can be omitted, id can be resolved without supplying an argument and may be referenced by the @is selection map.

Example № 89type Query {
  productById(id: ID! @is(field: "id")): Product @lookup
}

type Product {
  id(scope: IdScope): ID!
  name: String
}

In this counter-example, the @is selection map supplies an argument on id, violating the rule.

Counter Example № 90type Query {
  productByLocalId(id: ID! @is(field: "id(scope: LOCAL)")): Product @lookup
}

type Product {
  id(scope: IdScope): ID!
  name: String
}

3.1.5Validate Key Directives

3.1.5.1Key Fields Select Invalid Type

Error Code

KEY_FIELDS_SELECT_INVALID_TYPE

Severity

ERROR

Formal Specification
  • Let types be the set of all object or interface types that are annotated with the @key directive in the schema.
  • For each type in types:
    • Let keyDirectives be the set of all @key directives on type.
    • For each keyDirective in keyDirectives
      • Let keyFields be the set of all fields (including nested) referenced by the fields argument of keyDirective.
      • For each field in keyFields:
        • Let fieldType be the type of field.
        • fieldType must not be a List, Interface, or Union type.
Explanatory Text

The @key directive is used to define the set of fields that uniquely identify an entity. These fields must reference scalars or object types to ensure a valid and consistent representation of the entity across schemas. Fields of types List, Interface, or Union cannot be part of a @key because they do not have a well-defined unique value.

Examples

In this valid example, the Product type has a valid @key directive referencing the scalar field sku.

Example № 91type Product @key(fields: "sku") {
  sku: String!
  name: String
}

In the following counter-example, the Product type has an invalid @key directive referencing a field (featuredItem) whose type is an interface, violating the rule.

Counter Example № 92type Product @key(fields: "featuredItem { id }") {
  featuredItem: Node!
  sku: String!
}

interface Node {
  id: ID!
}

In this counter example, the @key directive references a field (tags) of type List, which is also not allowed.

Counter Example № 93type Product @key(fields: "tags") {
  tags: [String!]!
  sku: String!
}

In this counter example, the @key directive references a field (relatedItems) of type Union, which violates the rule.

Counter Example № 94type Product @key(fields: "relatedItems") {
  relatedItems: Related!
  sku: String!
}

union Related = Product | Service

type Service {
  id: ID!
}

3.1.5.2Key Directive in Fields Argument

Error Code

KEY_DIRECTIVE_IN_FIELDS_ARGUMENT

Severity

ERROR

Formal Specification
  • Let types be the set of all object and interface types in the schema.
  • For each type in types:
    • Let keyDirectives be the set of all @key directives on type.
    • For each keyDirective in keyDirectives:
      • Let fields be the string value of the fields argument of keyDirective.
      • fields must not contain a directive application.
Explanatory Text

The @key directive specifies the set of fields used to uniquely identify an entity. The fields argument must consist of a valid GraphQL selection set that does not include any directive applications. Directives in the fields argument are not supported.

Examples

In this example, the fields argument of the @key directive does not include any directive applications, satisfying the rule.

Example № 95type User @key(fields: "id name") {
  id: ID!
  name: String
}

In this counter-example, the fields argument of the @key directive includes a directive application @lowercase, which is not allowed.

Counter Example № 96directive @lowercase on FIELD_DEFINITION

type User @key(fields: "id name @lowercase") {
  id: ID!
  name: String
}

In this example, the fields argument includes a directive application @lowercase nested inside the selection set, which is also invalid.

Counter Example № 97directive @lowercase on FIELD_DEFINITION

type User @key(fields: "id name { firstName @lowercase }") {
  id: ID!
  name: FullName
}

type FullName {
  firstName: String
  lastName: String
}

3.1.5.3Key Fields Has Arguments

Error Code

KEY_FIELDS_HAS_ARGUMENTS

Severity

ERROR

Formal Specification
  • Let types be the set of all object and interface types in the schema that are annotated with the @key directive.
  • For each type in types:
    • Let keyDirectives be the set of all @key directives on type.
    • For each keyDirective in keyDirectives:
      • Let selections be the field selections of the fields argument of keyDirective.
      • For each selection in selections:
KeyFieldsHasArguments(selection, type)
  1. Let field be the field of type selected by selection.
  2. If selection supplies arguments:
    1. return true
  3. For each argumentDefinition declared by field:
    1. If the type of argumentDefinition is Non-Null, argumentDefinition has no default value, and argumentDefinition is not annotated with @require:
      1. return true
  4. If selection has a selection set:
    1. Let subType be the return type of field.
    2. Let subSelections be the selections in the selection set of selection.
    3. For each subSelection in subSelections:
      1. If KeyFieldsHasArguments(subSelection, subType) is true:
        1. return true
  5. return false
Explanatory Text

The @key directive designates the fields that form a stable key of an entity. Selections within the fields argument must not supply arguments: a stable key must consist of plain field values that identify an entity deterministically, and the distributed GraphQL executor resolves key fields without supplying any arguments.

A referenced field may still declare arguments, as long as each argument is nullable, has a default value, or is annotated with @require and therefore supplied by the executor. Such a field can be resolved without any arguments being supplied. A field that requires an argument cannot be part of a stable key because it cannot be resolved without one.

The same applies to @is (see Is Fields Has Arguments). @provides selections must reference fields that declare no arguments other than @require-annotated ones (see Provides Fields Has Arguments). Only @require selection maps may supply constant arguments, as they derive input values rather than keys.

Examples

In this example, the User type has a valid @key directive that references the argument-free fields id and name.

Example № 98type User @key(fields: "id name") {
  id: ID!
  name: String
  tags: [String]
}

In this example, the @key directive references the field tags, which declares the optional limit argument. Since limit can be omitted, tags can be resolved without supplying an argument and may be part of the key.

Example № 99type User @key(fields: "id tags") {
  id: ID!
  tags(limit: Int = 10): [String]
}

In this counter-example, the key selection supplies arguments on id. Selections within the fields argument must not supply arguments.

Counter Example № 100type User @key(fields: "id(scope: LOCAL)") {
  id: ID!
}

In this counter-example, the @key directive references the field tags, which requires the limit argument. Since limit can neither be omitted nor supplied by the key selection, tags cannot be part of a key.

Counter Example № 101type User @key(fields: "id tags") {
  id: ID!
  tags(limit: Int!): [String]
}

3.1.5.4Key Invalid Syntax

Error Code

KEY_INVALID_SYNTAX

Severity

ERROR

Formal Specification
  • Let types be the set of all object or interface types in the schema.
  • For each type in types:
    • Let keyDirectives be the set of all @key directives on type.
    • For each keyDirective in keyDirectives:
      • Let fieldsArg be the string value of the fields argument of keyDirective.
      • Attempt to parse fieldsArg as a valid GraphQL selection set.
      • Parsing must not fail (e.g., missing braces, invalid tokens, unbalanced curly braces, or other syntax errors).
Explanatory Text

Each @key directive must specify the fields that uniquely identify an entity using a valid GraphQL selection set in its fields argument. If the fields argument string is syntactically incorrect-missing closing braces, containing invalid tokens, or otherwise malformed – it cannot be composed into a valid schema and triggers the KEY_INVALID_SYNTAX error.

Examples

In this valid scenario, the fields argument is a correctly formed selection set: "sku featuredItem { id }" is properly balanced and contains no syntax errors.

Example № 102type Product @key(fields: "sku featuredItem { id }") {
  sku: String!
  featuredItem: Node!
}

interface Node {
  id: ID!
}

Here, the selection set "featuredItem { id" is missing the closing brace }. It is thus invalid syntax, causing a KEY_INVALID_SYNTAX error.

Counter Example № 103type Product @key(fields: "featuredItem { id") {
  featuredItem: Node!
  sku: String!
}

interface Node {
  id: ID!
}

3.1.5.5Key Invalid Fields

Error Code

KEY_INVALID_FIELDS

Severity

ERROR

Formal Specification
  • Let types be the set of all object and interface types in the schema.
  • For each type in types:
    • Let keyDirectives be the set of all @key directives on type.
    • For each keyDirective in keyDirectives:
      • Let fieldsArg be the string value of the fields argument of keyDirective.
      • Let selections be the set of fields in the selection set of fieldsArg.
      • For each selection in selections:
IsValidKeyField(selection, type)
  1. If selection is not defined on type:
    1. return false
  2. If selection has a selection set:
    1. Let subType be the return type of field.
    2. Let subFields be the set of all fields in the selection set of field.
    3. For each subField in subFields:
      1. IsValidKeyField(subField, subType) must be true.
  3. return true
Explanatory Text

Even if the selection set for @key(fields: "…") is syntactically valid, field references within that selection set must also refer to actual fields on the annotated type. This includes nested selections, which must appear on the corresponding return type. If any referenced field is missing or incorrectly named, composition fails with a KEY_INVALID_FIELDS error because the stable key cannot be resolved correctly.

Examples

In this valid example, the fields argument of the @key directive is properly defined with valid syntax and references existing fields.

Example № 104type Product @key(fields: "sku featuredItem { id }") {
  sku: String!
  featuredItem: Node!
}

interface Node {
  id: ID!
}

In this counter-example, the fields argument of the @key directive references a field id, which does not exist on the Product type.

Counter Example № 105type Product @key(fields: "id") {
  sku: String!
}

3.1.5.6Key Invalid Fields Type

Error Code

KEY_INVALID_FIELDS_TYPE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all composite types in schema.
  • For each type in types:
    • If type is annotated with @key:
      • Let fieldsArg be the value of the fields argument in the @key directive.
      • fieldsArg must be a string.
Explanatory Text

The @key directive designates the fields used to identify a particular object uniquely. The fields argument accepts a string that represents a selection set (for example, "id", or "id otherField"). If the fields argument is provided as any non-string type (e.g., Boolean, Int, Array), the schema fails to compose correctly because it cannot parse a valid field selection.

Examples

In this example, the @key directive’s fields argument is the string "id uuid", identifying two fields that form a composite stable key. This usage is valid.

Example № 106type User @key(fields: "id uuid") {
  id: ID!
  uuid: ID!
  name: String
}

type Query {
  users: [User]
}

Here, the fields argument is provided as a boolean (true) instead of a string. This violates the directive requirement and triggers a KEY_INVALID_FIELDS_TYPE error.

Counter Example № 107type User @key(fields: true) {
  id: ID
}

3.1.5.7Interface Object Key Missing

Error Code

INTERFACE_OBJECT_KEY_MISSING

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all object types in schema annotated with @interfaceObject.
  • For each type in types:
    • Let keyDirectives be the set of all @key directives on type.
    • keyDirectives must not be empty.
Explanatory Text

An object type annotated with @interfaceObject stands in for an interface defined in one or more other source schemas. The composite schema resolves this stand-in as an independently queryable entity. The distributed executor must be able to fetch it by its key, or extend it with the fields it contributes. The type must therefore declare at least one @key, exactly as any other entity type would. A stand-in with no key cannot be targeted by the executor and cannot contribute fields to the interface it stands in for.

This rule only requires that a key exists. The fields selected by the key are validated like those of any other @key (see Validate Key Directives). Whether the key matches one of the keys declared on the interface is validated across source schemas by Interface Object Key Mismatch.

Examples

In this example, the Media stand-in declares a @key, so source schema B’s contribution can be joined to the Media interface by id.

Example № 108# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  id: ID!
  rating: Int!
}

In the following counter-example, the Media stand-in declares no @key, so the entity it stands in for cannot be resolved. This results in an INTERFACE_OBJECT_KEY_MISSING error.

Counter Example № 109# Source Schema B
type Media @interfaceObject {
  id: ID!
  reviews: [Review!]!
}

type Review {
  id: ID!
  rating: Int!
}

3.1.6Validate Lookup Directives

3.1.6.1Lookup Must Have Arguments

Error Code

LOOKUP_MUST_HAVE_ARGUMENTS

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let fields be the set of all field definitions annotated with @lookup in schema.
  • For each field in fields:
    • The number of arguments on field must be greater than zero.
Explanatory Text

Fields annotated with the @lookup directive identify a single entity by the arguments supplied to them. A lookup field that declares no arguments has no stable key with which to resolve an entity and cannot participate in composition. This rule reports such fields as invalid.

Examples

For example, the following usage is valid because productById declares an argument that can be used to resolve a Product entity.

Example № 110type Query {
  productById(id: ID!): Product @lookup
}

type Product {
  id: ID!
  name: String
}

This counter-example demonstrates an invalid usage. The product field is annotated with @lookup but declares no arguments, so it cannot identify which entity to resolve.

Counter Example № 111type Query {
  product: Product @lookup
}

type Product {
  id: ID!
  name: String
}

3.1.6.2Lookup Returns Non-Nullable Type

Error Code

LOOKUP_RETURNS_NON_NULLABLE_TYPE

Severity

WARNING

Formal Specification
  • Let schema be the source schema to validate.
  • Let fields be the set of all field definitions annotated with @lookup in schema.
  • For each field in fields:
    • Let type be the return type of field.
    • type must be a nullable type.
Explanatory Text

Fields annotated with the @lookup directive are intended to retrieve a single entity based on provided arguments. To properly handle cases where the requested entity does not exist, such fields should have a nullable return type. This allows the field to return null when an entity matching the provided criteria is not found, following the standard GraphQL practices for representing missing data.

In a distributed system, it is likely that some entities will not be found on other schemas, even when those schemas contribute fields to the type. Ensuring that @lookup fields have nullable return types also avoids GraphQL errors on schemas and prevents result erasure through non-null propagation. By allowing null to be returned when an entity is not found, the system can gracefully handle missing data without causing exceptions or unexpected behavior.

Ensuring that @lookup fields have nullable return types allows gateways to distinguish between cases where an entity is not found (receiving null) and other error conditions that may have to be propagated to the client.

For example, the following usage is recommended:

Example № 112type Query {
  userById(id: ID!): User @lookup
}

type User {
  id: ID!
  name: String
}

In this example, userById returns a nullable User type, aligning with the recommendation.

Examples

This counter-example demonstrates an invalid usage:

Counter Example № 113type Query {
  userById(id: ID!): User! @lookup
}

type User {
  id: ID!
  name: String
}

Here, userById returns a non-nullable User!, which does not align with the recommendation that a @lookup field should have a nullable return type.

3.1.6.3Lookup Returns List

Error Code

LOOKUP_RETURNS_LIST

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let fields be the set of all field definitions annotated with @lookup in schema.
  • For each field in fields:
    • Let type be the return type of field.
    • IsListType(type) must be false.
IsListType(type)
  1. If type is a Non-Null type:
    1. Let innerType be the inner type of type.
    2. Return IsListType(innerType).
  2. Else if type is a List type:
    1. Return true.
  3. Else:
    1. Return false.
Explanatory Text

Fields annotated with the @lookup directive are intended to retrieve a single entity based on provided arguments. To avoid ambiguity in entity resolution, such fields must return a single object and not a list. This validation rule enforces that any field annotated with @lookup must have a return type that is NOT a list.

Examples

For example, the following usage is valid:

Example № 114type Query {
  userById(id: ID!): User @lookup
}

type User {
  id: ID!
  name: String
}

In this example, userById returns a User object, satisfying the requirement.

This counter-example demonstrates an invalid usage:

Counter Example № 115type Query {
  usersByIds(ids: [ID!]!): [User!] @lookup
}

type User {
  id: ID!
  name: String
}

Here, usersByIds returns a list of User objects, which violates the requirement that a @lookup field must return a single object.

3.1.6.4Lookup Key Missing For Type

Error Code

LOOKUP_KEY_MISSING_FOR_TYPE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let lookupFields be the set of all fields in schema annotated with @lookup.
  • For each lookupField in lookupFields:
    • Let returnType be the unwrapped return type of lookupField.
    • Let possibleTypes be GetPossibleTypes(returnType).
    • For each argument in the arguments of lookupField:
      • For each possibleType in possibleTypes:
IsArgumentMappable(argument, possibleType)
  1. If argument is annotated with @is:
    1. Let selectionMap be the parsed selection map of the field argument of the @is directive on argument.
    2. For each alternative in the alternatives of selectionMap:
      1. If the type conditions of alternative admit possibleType and all fields referenced by alternative are defined for possibleType:
        1. return true
    3. return false
  2. Otherwise:
    1. Let argumentName be the name of argument.
    2. If possibleType defines a field named argumentName:
      1. return true
    3. return false
Explanatory Text

A lookup field must be able to resolve every possible runtime type of its return type. The arguments of a lookup field represent the stable key with which an entity is resolved, and each argument must independently be mappable to a field for every possible object type of the return type.

Without an @is directive, an argument is mapped by its name: every possible object type must define a field with the argument’s name, either directly or through an interface. With an @is directive, the selection map defines the mapping, and its alternatives may map different runtime types to different fields. The alternatives must still cover every possible type: a runtime type that is not covered by any alternative cannot be resolved by the lookup field. A source schema that can only resolve a subset of the possible types must declare a narrower return type instead.

Examples

In this example, the selection map covers all possible types of Media, resolving each by a different key field.

Example № 116type Query {
  mediaByKey(
    key: MediaKeyInput!
      @is(
        field: "{ isbn: <Book>.isbn } | { upc: <Movie>.upc } | { feedUrl: <Podcast>.feedUrl }"
      )
  ): Media @lookup
}

input MediaKeyInput @oneOf {
  isbn: String
  upc: String
  feedUrl: String
}

interface Media {
  id: ID!
}

type Book implements Media {
  id: ID!
  isbn: String!
}

type Movie implements Media {
  id: ID!
  upc: String!
}

type Podcast implements Media {
  id: ID!
  feedUrl: String!
}

In this counter-example, the selection map covers only Book and Movie. Podcast is a possible type of Media but is not covered by any alternative, violating the rule.

Counter Example № 117type Query {
  mediaByKey(
    key: MediaKeyInput!
      @is(field: "{ isbn: <Book>.isbn } | { upc: <Movie>.upc }")
  ): Media @lookup
}

input MediaKeyInput @oneOf {
  isbn: String
  upc: String
}

interface Media {
  id: ID!
}

type Book implements Media {
  id: ID!
  isbn: String!
}

type Movie implements Media {
  id: ID!
  upc: String!
}

type Podcast implements Media {
  id: ID!
  feedUrl: String!
}

In this counter-example, the arguments are mapped by name, but the possible type Clothing does not define a field named categoryId, violating the rule.

Counter Example № 118type Query {
  product(id: ID!, categoryId: Int): Product @lookup
}

union Product = Electronics | Clothing

type Electronics {
  id: ID!
  categoryId: Int
  name: String
}

type Clothing {
  id: ID!
  name: String
}

3.1.7Validate Override Directives

3.1.7.1Override from Self

Error Code

OVERRIDE_FROM_SELF

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all composite types in schema.
  • For each type in types:
    • Let fields be the set of fields on type.
    • For each field in fields:
      • If field is annotated with @override:
        • Let from be the value of the from argument of the @override directive on field.
        • from must not be the same as the name of schema:
Explanatory Text

When using @override, the from argument indicates the name of the source schema that originally owns the field. Overriding from the same schema creates a contradiction, as it implies both local and transferred ownership of the field within one schema. If the from value matches the local schema name, it triggers an OVERRIDE_FROM_SELF error.

Examples

In the following example, Schema B overrides the field amount from Schema A. The two schema names are different, so no error is raised.

Example № 119# Source Schema A
type Bill {
  id: ID!
  amount: Int
}

# Source Schema B
type Bill {
  id: ID!
  amount: Int @override(from: "SchemaA")
}

In the following counter-example, the local schema is also "SchemaA", and the from argument is "SchemaA". Overriding a field from the same schema is not allowed, causing an OVERRIDE_FROM_SELF error.

Counter Example № 120# Source Schema A (named "SchemaA")
type Bill {
  id: ID!
  amount: Int @override(from: "SchemaA")
}

3.1.7.2Override on Interface

Error Code

OVERRIDE_ON_INTERFACE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all interface types in schema.
  • For each type in types:
    • Let fields be the set of fields on type.
    • For each field in fields:
      • field must not be annotated with @override
Explanatory Text

The @override directive designates that ownership of a field is transferred from one source schema to another. In the context of interface types, fields are abstract—objects that implement the interface are responsible for providing the actual fields. Consequently, it is invalid to attach @override directly to an interface field. Doing so leads to an OVERRIDE_ON_INTERFACE error because there is no concrete field implementation on the interface itself that can be overridden.

Examples

In this valid example, @override is used on a field of an object type, ensuring that the field definition is concrete and can be reassigned to another schema.

Since @override is not used on any interface fields, no error is produced.

Example № 121# Source Schema A
type Order {
  id: ID!
  amount: Int
}

# Source Schema B
type Order {
  id: ID!
  amount: Int @override(from: "SchemaA")
}

In the following counter-example, Bill.amount is declared on an interface type and annotated with @override. This violates the rule because the interface field itself is not eligible for ownership transfer. The composition fails with an OVERRIDE_ON_INTERFACE error.

Counter Example № 122# Source Schema A
interface Bill {
  id: ID!
  amount: Int @override(from: "SchemaB")
}

3.1.8Validate Provides Directives

3.1.8.1Provides Directive in Fields Argument

Error Code

PROVIDES_DIRECTIVE_IN_FIELDS_ARGUMENT

Severity

ERROR

Formal Specification
  • Let fieldsWithProvides be the set of all fields annotated with the @provides directive in the schema.
  • For each field in fieldsWithProvides:
    • Let fields be the selected fields of the fields argument of the @provides directive on field.
    • For each selection in fields:
HasProvidesDirective(selection)
  1. If selection has a directive application:
    1. return true
  2. If selection has a selection set:
    1. Let subSelections be the selections in selection
    2. For each subSelection in subSelections:
      1. If HasProvidesDirective(subSelection) is true
        1. return true
Explanatory Text

The @provides directive is used to specify the set of fields on an object type that a resolver provides for the parent type. The fields argument must consist of a valid GraphQL selection set without any directive applications, as directives within the fields argument are not supported.

Examples

In this example, the fields argument of the @provides directive does not have any directive applications, satisfying the rule.

Example № 123type User @key(fields: "id name") {
  id: ID!
  name: String
  profile: Profile @provides(fields: "name")
}

type Profile {
  id: ID!
  name: String
}

In this counter-example, the fields argument of the @provides directive has a directive application @lowercase, which is not allowed.

Counter Example № 124directive @lowercase on FIELD_DEFINITION

type User @key(fields: "id name") {
  id: ID!
  name: String
  profile: Profile @provides(fields: "name @lowercase")
}

type Profile {
  id: ID!
  name: String
}

3.1.8.2Provides Fields Has Arguments

Error Code

PROVIDES_FIELDS_HAS_ARGUMENTS

Severity

ERROR

Formal Specification
  • Let fieldsWithProvides be the set of all fields annotated with the @provides directive in the schema.
  • For each field in fieldsWithProvides:
    • Let selections be the field selections of the fields argument of the @provides directive on field.
    • Let type be the return type of field.
    • For each selection in selections:
ProvidesHasArguments(selection, type)
  1. Let field be the field of type selected by selection.
  2. If field declares an argument that is not annotated with @require:
    1. return true
  3. If selection supplies arguments:
    1. return true
  4. If selection has a selection set:
    1. Let subType be the return type of field.
    2. Let subSelections be the selections in the selection set of selection.
    3. For each subSelection in subSelections:
      1. If ProvidesHasArguments(subSelection, subType) is true:
        1. return true
  5. return false
Explanatory Text

The @provides directive specifies fields that a resolver provides for the parent type. The fields argument must reference fields that do not declare arguments, as fields with arguments introduce variability that is incompatible with the consistent behavior expected of @provides. Arguments annotated with @require are exempt, since they are supplied by the executor rather than chosen by the consumer. A selection within the fields argument must never supply arguments.

Note Unlike @require, which describes how to derive a value (and may therefore include constant arguments to disambiguate the selection), @provides advertises that the resolver returns the listed fields as part of its parent’s selection set. Because the consumer chooses the arguments at query time, the resolver cannot pre-commit to a specific parameterization, and constant arguments would be meaningless here.
Examples
Example № 125type User @key(fields: "id") {
  id: ID!
  tags: [String]
}

type Article @key(fields: "id") {
  id: ID!
  author: User! @provides(fields: "tags")
}

In this example, the tags field declares only an argument annotated with @require. Since its value is supplied by the executor, tags may still be referenced by the @provides selection.

Example № 126type User @key(fields: "id") {
  id: ID!
  tags(limit: Int @require(field: "tagLimit")): [String]
  tagLimit: Int
}

type Article @key(fields: "id") {
  id: ID!
  author: User! @provides(fields: "tags")
}

This violates the rule because the tags field referenced in the fields argument of the @provides directive is defined with arguments (limit: UserType = ADMIN).

Counter Example № 127type User @key(fields: "id") {
  id: ID!
  tags(limit: UserType = ADMIN): [String]
}

enum UserType {
  REGULAR
  ADMIN
}

type Article @key(fields: "id") {
  id: ID!
  author: User! @provides(fields: "tags")
}

In this counter-example, the @provides selection supplies arguments on tags, even though tags does not declare any. Selections within the fields argument must not supply arguments.

Counter Example № 128type User @key(fields: "id") {
  id: ID!
  tags: [String]
}

type Article @key(fields: "id") {
  id: ID!
  author: User! @provides(fields: "tags(limit: 10)")
}

3.1.8.3Provides Fields Missing External

Error Code

PROVIDES_FIELDS_MISSING_EXTERNAL

Severity

ERROR

Formal Specification
  • Let objectTypes be the set of all object types in the schema.
  • For each objectType in objectTypes:
    • Let providingFields be the set of fields on objectType annotated with @provides.
    • For each field in providingFields:
      • Let referencedFields be the set of fields referenced by the fields argument of the @provides directive on field.
      • For each referencedField in referencedFields:
        • If referencedField is not marked as @external
          • Produce a PROVIDES_FIELDS_MISSING_EXTERNAL error.
Explanatory Text

The @provides directive indicates that an object type field will supply additional fields belonging to the return type in this execution-specific path. Any field listed in the @provides(fields: ...) argument must therefore be external in the local schema, meaning that the local schema itself does not provide it.

This rule disallows selecting non-external fields in a @provides selection set. If a field is already provided by the same schema in all execution paths, there is no need to @provide.

Examples

Here, the Order type from this schema is providing fields on User through @provides. The name field of User is not defined in this schema; it is declared with @external indicating that the name field comes from elsewhere. Thus, referencing name under @provides(fields: "name") is valid.

Example № 129type Order {
  id: ID!
  customer: User @provides(fields: "name")
}

type User @key(fields: "id") {
  id: ID!
  name: String @external
}

In this counter-example, User.address is not marked as @external in the same schema that applies @provides. This means the schema already provides the address field in all possible paths, so using @provides(fields: "address") is invalid.

Counter Example № 130type User {
  id: ID!
  address: String
}

type Order {
  id: ID!
  buyer: User @provides(fields: "address")
}

3.1.8.4Provides Invalid Syntax

Error Code

PROVIDES_INVALID_SYNTAX

Severity

ERROR

Formal Specification
  • Let fieldsWithProvides be the set of all fields annotated with the @provides directive in the schema.
  • For each field in fieldsWithProvides:
    • Let fieldsArg be the string value of the fields argument of the @provides directive on field.
    • fieldsArg must be a valid selection set string
Explanatory Text

The @provides directive’s fields argument must be a syntactically valid selection set string, as if you were selecting fields in a GraphQL query. If the selection set is malformed (e.g., missing braces, unbalanced quotes, or invalid tokens), the schema composition fails with a PROVIDES_INVALID_SYNTAX error.

Examples

Here, the @provides directive’s fields argument is a valid selection set:

Example № 131type User @key(fields: "id") {
  id: ID!
  address: Address @provides(fields: "street city")
}

type Address {
  street: String
  city: String
}

In this counter-example, the fields argument is missing a closing brace. It cannot be parsed as a valid GraphQL selection set, triggering a PROVIDES_INVALID_SYNTAX error.

Counter Example № 132type User @key(fields: "id") {
  id: ID!
  address: Address @provides(fields: "{ street city ")
}

3.1.8.5Provides Invalid Fields

Error Code

PROVIDES_INVALID_FIELDS

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let fieldsWithProvides be the set of all fields annotated with the @provides directive in schema.
  • For each field in fieldsWithProvides:
    • Let fieldsArg be the string value of the fields argument of the @provides directive on field.
    • Let parsedFieldSelectionSet be the parsed field selection set from fieldsArg.
    • Let returnType be the return type of field.
    • ValidateFieldSelectionSet(parsedFieldSelectionSet, returnType) must be true.
ValidateFieldSelectionSet(fieldSelectionSet, parentType)
  1. For each selection in fieldSelectionSet:
    1. Let selectedField be the field selected by selection in parentType.
    2. If selectedField does not exist on parentType:
      1. return false
    3. Let selectedType be the type of selectedField
    4. If selectedType is an INTERFACE or OBJECT type
      1. Let subSelectionSet be the field selection set of selection
      2. If subSelectionSet is empty
        1. return false
      3. If ValidateFieldSelectionSet(subSelectionSet, fieldType) is false
        1. return false
  2. return true
Explanatory Text

Even if the @provides(fields: "…") argument is well-formed syntactically, the selected fields must actually exist on the return type of the field. Invalid field references—e.g., selecting non-existent fields, referencing fields on the wrong type, or incorrectly omitting required nested selections—lead to a PROVIDES_INVALID_FIELDS error.

Examples

In the following example, the @provides directive references a valid field (hobbies) on the UserDetails type.

Example № 133type User @key(fields: "id") {
  id: ID!
  details: UserDetails @provides(fields: "hobbies")
}

type UserDetails {
  hobbies: [String]
}

In the following counter-example, the @provides directive specifies a field named unknownField which is not defined on UserDetails. This raises a PROVIDES_INVALID_FIELDS error.

Counter Example № 134type User @key(fields: "id") {
  id: ID!
  details: UserDetails @provides(fields: "unknownField")
}

type UserDetails {
  hobbies: [String]
}

3.1.8.6Provides Invalid Fields Type

Error Code

PROVIDES_INVALID_FIELDS_TYPE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all composite types in schema.
  • For each type in types:
    • Let fields be the set of fields on type.
    • For each field in fields:
      • If field is annotated with @provides:
        • Let fieldsArg be the value of the fields argument on the @provides directive.
        • fieldsArg must be a string.
Explanatory Text

The @provides directive indicates that a field is providing one or more additional fields on the returned (child) type. The fields argument accepts a string representing a GraphQL selection set (for example, "title author"). If the fields argument is given as a non-string type (e.g., Boolean, Int, Array), the schema fails to compose because it cannot interpret a valid selection set.

Examples

In this valid example, the @provides directive on details uses the string "features specifications" to specify that both fields are provided in the child type ProductDetails.

Example № 135type Product {
  id: ID!
  details: ProductDetails @provides(fields: "features specifications")
}

type ProductDetails {
  features: [String]
  specifications: String
}

type Query {
  products: [Product]
}

Here, the @provides directive includes a numeric value (123) instead of a string in its fields argument. This invalid usage raises a PROVIDES_INVALID_FIELDS_TYPE error.

Counter Example № 136type Product {
  id: ID!
  details: ProductDetails @provides(fields: 123)
}

type ProductDetails {
  features: [String]
  specifications: String
}

3.1.8.7Provides on Non-Composite Field

Error Code

PROVIDES_ON_NON_COMPOSITE_FIELD

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of all object and interface types in schema.
  • For each type in types:
    • Let fields be the set of fields on type.
    • For each field in fields:
      • If field is annotated with @provides:
        • Let fieldType be the base return type of field (i.e., unwrapped of any [ ] or !).
        • fieldType must be an interface or object type.
Explanatory Text

The @provides directive allows a field to “provide” additional nested fields on the composite type it returns. If a field’s base type is not an object or interface type (e.g., String, Int, Boolean, Enum, Union, or an Input type), it cannot hold nested fields for @provides to select. Consequently, attaching @provides to such a field is invalid and raises a PROVIDES_ON_NON_COMPOSITE_FIELD error.

Examples

Here, profile has an object base type Profile. The @provides directive can validly specify sub-fields like settings { theme }.

Example № 137type Profile {
  email: String
  settings: Settings
}

type Settings {
  notificationsEnabled: Boolean
  theme: String
}

type User {
  id: ID!
  profile: Profile @provides(fields: "settings { theme }")
}

In this counter-example, email has a scalar base type (String). Because scalars do not expose sub-fields, attaching @provides to email triggers a PROVIDES_ON_NON_COMPOSITE_FIELD error.

Counter Example № 138type User {
  id: ID!
  email: String @provides(fields: "length")
}

3.1.9Validate Require Directives

3.1.9.1Require Invalid Syntax

Error Code

REQUIRE_INVALID_SYNTAX

Severity

ERROR

Formal Specification
  • Let compositeTypes be the set of all composite types in the schema.
  • For each composite in compositeTypes:
    • Let fields be the set of fields on composite.
    • Let arguments be the set of all arguments on fields.
    • For each argument in arguments:
      • If argument is not annotated with @require:
        • Continue
      • Let fieldArg be the string value of the field argument of the @require directive on argument.
      • fieldArg must be be parsable as a valid selection map
Explanatory Text

The @require directive’s field argument must be syntactically valid GraphQL. If the selection map string is malformed (e.g., missing closing braces, unbalanced quotes, invalid tokens), then the schema cannot be composed correctly. In such cases, the error REQUIRE_INVALID_SYNTAX is raised.

Examples

In the following example, the @require directive’s field argument is a valid selection map and satisfies the rule.

Example № 139type User @key(fields: "id") {
  id: ID!
  profile(name: String @require(field: "name")): Profile
}

type Profile {
  id: ID!
  name: String
}

In the following counter-example, the @require directive’s field argument has invalid syntax because it is missing a closing brace.

This violates the rule and triggers a REQUIRE_INVALID_SYNTAX error.

Counter Example № 140type User @key(fields: "id") {
  id: ID!
  profile(name: String! @require(field: "{ name ")): Profile
}

type Profile {
  id: ID!
  name: String
}

3.1.9.2Require Invalid Fields Type

Error Code

REQUIRE_INVALID_FIELD_TYPE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let compositeTypes be the set of all composite types in schema.
  • For each composite in compositeTypes:
    • Let fields be the set of fields on composite.
    • Let arguments be the set of all arguments on fields.
    • For each argument in arguments:
      • If argument is not annotated with @require:
        • Continue
      • Let fieldArg be the value of the field argument of the @require directive on argument.
      • If fieldArg is not a string:
        • Produce a REQUIRE_INVALID_FIELD_TYPE error.
Explanatory Text

When using the @require directive, the field argument must always be a string that defines a (potentially nested) selection set of fields from the same type. If the field argument is provided as a type other than a string (such as an integer, boolean, or enum), the directive usage is invalid and will cause schema composition to fail.

Examples

In the following example, the @require directive’s field argument is a valid string and satisfies the rule.

Example № 141type User @key(fields: "id") {
  id: ID!
  profile(name: String @require(field: "name")): Profile
}

type Profile {
  id: ID!
  name: String
}

Since field is set to 123 (an integer) instead of a string, this violates the rule and triggers a REQUIRE_INVALID_FIELD_TYPE error.

Counter Example № 142type User @key(fields: "id") {
  id: ID!
  profile(name: String! @require(field: 123)): Profile
}

type Profile {
  id: ID!
  name: String
}

3.1.9.3Require Invalid Usage

Error Code

REQUIRE_INVALID_USAGE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let compositeTypes be the set of all composite types in schema.
  • For each compositeType in compositeTypes:
    • Let fields be the set of fields on compositeType.
    • For each field in fields:
      • If field is annotated with @lookup:
        • Let arguments be the set of all arguments on field.
        • For each argument in arguments:
          • argument must not be annotated with @require
Explanatory Text

The arguments of a lookup field represent the stable key with which the distributed GraphQL executor resolves an entity. Their values are supplied from an existing representation of the entity – either directly by argument name or through an @is mapping – before the lookup is executed.

The @require directive, in contrast, expresses a data dependency of a field that is resolved in the context of an existing parent object. A lookup field is used to establish that context in the first place; for a lookup field reachable from the root Query type, no parent entity exists from which a requirement could be fulfilled. The satisfiability validation likewise describes lookup inputs solely through @is mappings or argument names; an argument annotated with @require has no defined contribution to a lookup.

Therefore, annotating an argument of a lookup field with @require is invalid and raises a REQUIRE_INVALID_USAGE error.

Examples

In the following example, the lookup field productById resolves Product by its stable key, and the requirement is declared on the argument of an ordinary field, satisfying the rule.

Example № 143# Source Schema A
type Query {
  productById(id: ID!): Product @lookup
}

type Product @key(fields: "id") {
  id: ID!
  shippingCost(weight: Float @require(field: "shippingWeight")): Currency
}

# Source Schema B
type Product @key(fields: "id") {
  id: ID!
  shippingWeight: Float
}

In the following counter-example, the locale argument of the lookup field productById is annotated with @require, violating the rule.

Counter Example № 144# Source Schema A
type Query {
  productById(
    id: ID!
    locale: String @require(field: "defaultLocale")
  ): Product @lookup
}

type Product @key(fields: "id") {
  id: ID!
}

# Source Schema B
type Query {
  defaultLocale: String
}

3.1.9.4Require Inconsistent on Implementation

Error Code

REQUIRE_INCONSISTENT_ON_IMPLEMENTATION

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let implementingTypes be the set of all object and interface types in schema that implement at least one interface.
  • For each implementingType in implementingTypes:
    • Let interfaces be the set of interface types that implementingType implements.
    • For each interface in interfaces:
      • Let interfaceFields be the set of fields on interface.
      • For each interfaceField in interfaceFields:
        • Let implementingField be the field on implementingType with the same name as interfaceField.
        • For each interfaceArgument in the arguments of interfaceField:
          • Let implementingArgument be the argument on implementingField with the same name as interfaceArgument.
          • If interfaceArgument is annotated with @require:
            • implementingArgument must be annotated with @require
          • Otherwise:
            • implementingArgument must not be annotated with @require
Explanatory Text

The @require directive may be applied to arguments of fields declared on interface types. The selection map is rooted at the interface type and is evaluated against the concrete runtime object: fields declared on the interface can be selected without type conditions, while fields of specific implementing types can be referenced through type conditions.

GraphQL requires an implementing field to redeclare every argument of the interface field, and composition removes all arguments annotated with @require from the composite schema. The @require annotation must therefore be applied consistently across the interface contract: an argument is annotated with @require on the interface field and on the corresponding argument of every implementing field, or on neither. Consistent annotation removes the argument from the interface field and from all implementing fields together, so the composite schema retains a valid interface contract. Inconsistent annotation would remove the argument from only one side of the contract and break the composite schema.

The selection maps of the interface field argument and of an implementing field argument may differ: each is validated against its own declaring type, and an implementing type may derive the required value from implementation-specific fields.

Note Cross-schema cases in which a merged interface field declares an argument that an implementing field lacks are detected after merging by Interface Field Argument No Implementation.
Examples

In this example, the locale argument is annotated with @require on the interface field Account.displayName and on the implementing field User.displayName, satisfying the rule.

Example № 145# Source Schema A
interface Account {
  id: ID!
  displayName(locale: String @require(field: "preferredLocale")): String
}

type User implements Account @key(fields: "id") {
  id: ID!
  displayName(locale: String @require(field: "preferredLocale")): String
}

# Source Schema B
interface Account {
  id: ID!
  preferredLocale: String
}

type User implements Account @key(fields: "id") {
  id: ID!
  preferredLocale: String
}

In this counter-example, the locale argument is annotated with @require on the implementing field User.displayName but not on the interface field Account.displayName, violating the rule. The composite schema would declare locale on the interface field but not on the implementing field, breaking the interface contract.

Counter Example № 146# Source Schema A
interface Account {
  id: ID!
  displayName(locale: String): String
}

type User implements Account @key(fields: "id") {
  id: ID!
  displayName(locale: String @require(field: "preferredLocale")): String
}

# Source Schema B
type User @key(fields: "id") {
  id: ID!
  preferredLocale: String
}

3.1.10Validate Shareable Directives

3.1.10.1Invalid Shareable Usage

Error Code

INVALID_SHAREABLE_USAGE

Severity

ERROR

Formal Specification
  • Let schema be the source schema to validate.
  • Let types be the set of types defined in schema.
  • For each type in types:
    • If type is an interface type:
      • For each field definition field in type:
        • If field is annotated with @shareable, produce an INVALID_SHAREABLE_USAGE error.
    • If type is the Subscription type:
      • For each field definition field in type:
        • If field is annotated with @shareable, produce an INVALID_SHAREABLE_USAGE error.
Explanatory Text

The @shareable directive is intended to indicate that a field on an object type can be resolved by multiple schemas without conflict. As a result, it is only valid to use @shareable on fields of object types (or on the entire object type itself).

Applying @shareable to interface fields is disallowed and violates the valid usage of the directive. This rule prevents schema composition errors and data conflicts by ensuring that @shareable is used only in contexts where shared field resolution is meaningful and unambiguous.

Additionally, subscription root fields cannot be shared (i.e., they are effectively non-shareable), as subscription events from multiple schemas would create conflicts in the composed schema. Attempting to mark a subscription field as shareable or to define it in multiple schemas triggers the same error.

Examples

In this example, the field orderStatus on the Order object type is marked with @shareable, which is allowed. It signals that this field can be served from multiple schemas without creating a conflict.

Example № 147type Order {
  id: ID!
  orderStatus: String @shareable
  total: Float
}

In this counter-example, the InventoryItem interface has a field sku marked with @shareable, which is invalid usage. Marking an interface field as shareable leads to an INVALID_SHAREABLE_USAGE error.

Counter Example № 148interface InventoryItem {
  sku: ID! @shareable
  name: String
}

By definition, root subscription fields cannot be shared across multiple schemas. In this example, both schemas define a subscription field newOrderPlaced:

Counter Example № 149# Schema A
type Subscription {
  newOrderPlaced: Order @shareable
}

type Order {
  id: ID!
  items: [String]
}

# Schema B
type Subscription {
  newOrderPlaced: Order @shareable
}

3.2Pre Merge Validation

Prior to merging the schemas, additional validations are performed that require visibility into all source schemas but treat each source schema separately. This step detects conflicts such as incompatible fields or default argument values that would render the merged schema unusable. Detecting such conflicts early prevents errors that would otherwise be discovered during the merge process.

3.2.1Validate Type System

3.2.1.1Type Kind Mismatch

Error Code

TYPE_KIND_MISMATCH

Severity

ERROR

Formal Specification
  • Let schemas be the set of all source schemas.
  • For each type name typeName defined in at least one of these schemas:
    • Let types be the set of all types named typeName across all source schemas.
    • Let consideredTypes be the subset of types excluding any object type annotated with @interfaceObject.
    • All consideredTypes must be of the same kind (Object, Interface, Union, Enum, InputObject, Scalar).
Explanatory Text

Each named type must represent the same kind of GraphQL type across all source schemas. For instance, a type named User must consistently be an object type, or consistently be an interface, and so forth. If one schema defines User as an object type, while another schema declares User as an interface (or input object, union, etc.), the schema composition process cannot merge these definitions coherently.

A single type name cannot represent two different kinds of type in the composed schema.

The one exception is an object type annotated with @interfaceObject. Such a type is a stand-in for an interface of the same name (see Interface Object No Interface). Composition deliberately excludes it from this check. An @interfaceObject type and the interface it stands in for are not a kind mismatch. They are the mechanism by which a source schema contributes field implementations that composition projects onto an interface’s implementing types. An object type with the same name that is not annotated with @interfaceObject is still an ordinary kind mismatch.

Examples

All schemas agree that User is an object type:

Example № 150# Schema A
type User {
  id: ID!
  name: String
}

# Schema B
type User {
  id: ID!
  email: String
}

# Schema C
type User {
  id: ID!
  joinedAt: String
}

In the following counter-example, User is defined as an object type in one of the schemas and as an interface in another. This violates the rule and results in a TYPE_KIND_MISMATCH error.

Counter Example № 151# Schema A: `User` is an object type
type User {
  id: ID!
  name: String
}

# Schema B: `User` is an interface
interface User {
  id: ID!
  friends: [User!]!
}

Media is declared as an interface in source schema A and as an object type annotated with @interfaceObject in source schema B. Because the stand-in is annotated, it is excluded from the kind check and no error is raised.

Example № 152# Source Schema A: `Media` is an interface
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

# Source Schema B: `Media` is an `@interfaceObject` stand-in
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  id: ID!
  rating: Int!
}

Here, source schema B declares Media as a plain object type, without @interfaceObject. The exception does not apply, so the object type in source schema B and the interface in source schema A are a genuine kind mismatch, resulting in a TYPE_KIND_MISMATCH error.

Counter Example № 153# Source Schema A: `Media` is an interface
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

# Source Schema B: `Media` is a plain object type, not a stand-in
type Media {
  id: ID!
  reviewCount: Int!
}

3.2.2Validate Enums

3.2.2.1Enum Values Mismatch

Error Code

ENUM_VALUES_MISMATCH

Formal Specification
  • Let enumNames be the set of all enum type names across all source schemas.
  • For each enumName in enumNames:
    • Let enums be the list of all enum types from different source schemas with the name enumName.
    • EnumsAreMergeable(enums) must be true.
EnumsAreMergeable(enums)
  1. If enums has fewer than 2 elements:
    1. Return true.
  2. Let inaccessibleValues be the set of values that are declared as @inaccessible in enums.
  3. Let requiredValues be the set of values in enums that are not in inaccessibleValues.
  4. For each enum in enums
    1. Let enumValues be the set of all values of enum that are not in inaccessibleValues.
    2. requiredValues must be equal to enumValues
Explanatory Text

This rule ensures that enum types with the same name across different source schemas in a composite schema have identical sets of values. Enums must be consistent across source schemas to avoid conflicts and ambiguities in the composite schema.

When an enum is defined with differing values, it can lead to confusion and errors in query execution. For instance, a value valid in one schema might be passed to another where it’s unrecognized, leading to unexpected behavior or failures. This rule prevents such inconsistencies by enforcing that all instances of the same named enum across schemas have an exact match in their values.

In this example, both source schemas define Genre with the same value FANTASY, satisfying the rule:

Example № 154enum Genre {
  FANTASY
}

enum Genre {
  FANTASY
}

Here, the two definitions of Genre have different values (FANTASY and SCIENCE_FICTION), violating the rule:

Counter Example № 155enum Genre {
  FANTASY
}

enum Genre {
  SCIENCE_FICTION
}

Here, the two definitions of Genre have shared values and additional values declared as @inaccessible, satisfying the rule:

Example № 156enum Genre {
  FANTASY
  SCIENCE_FICTION @inaccessible
}

enum Genre {
  FANTASY
}

3.2.3Validate Composite Types

3.2.3.1Output Field Types Mergeable

Error Code

OUTPUT_FIELD_TYPES_NOT_MERGEABLE

Severity

ERROR

Formal Specification
  • Let typeNames be the set of all output type names from all source schemas.
  • For each typeName in typeNames
    • Let types be the set of all types with the name typeName from all source schemas.
    • Let fieldNames be the set of all field names from all types.
    • For each fieldName in fieldNames
      • Let fields be the set of all fields with the name fieldName from all types.
      • FieldsAreMergeable(fields) must be true.
FieldsAreMergeable(fields)
  1. Let fieldTypes be the list of types of each field in fields.
  2. LeastRestrictiveType(fieldTypes) must not fail.
Explanatory Text

Fields on objects or interfaces that have the same name are considered semantically equivalent and mergeable when LeastRestrictiveType(fieldTypes) can select a return type for the composed field. This selection considers all field types together and must not depend on source schema order.

Fields with the same type are mergeable.

Example № 157type User {
  birthdate: String
}

type User {
  birthdate: String
}

Fields with different nullability are mergeable, resulting in a merged field with a nullable type.

Example № 158type User {
  birthdate: String!
}

type User {
  birthdate: String
}
Example № 159type User {
  tags: [String!]
}

type User {
  tags: [String]!
}

type User {
  tags: [String]
}

Fields with leaf return types are not mergeable if the named types differ, or if the same name is used with a different kind.

Counter Example № 160type User {
  birthdate: String!
}

type User {
  birthdate: DateTime!
}
Counter Example № 161type User {
  tags: [Tag]
}

type Tag {
  value: String
}

type User {
  tags: [Tag]
}

scalar Tag

Fields with composite return types are mergeable when one of the declared return types is a supertype of all other declared return types. The composed field uses that supertype, regardless of the order in which the source schemas are processed.

Example № 162# Schema A
type Query @shareable {
  featured: FeaturedItem
}

union FeaturedItem = Product

type Product @shareable {
  id: ID
}

# Schema B
type Query @shareable {
  featured: Product
}

type Product @shareable {
  id: ID
}

# Composed Result
type Query {
  featured: FeaturedItem
}

union FeaturedItem = Product

type Product {
  id: ID
}

Fields with composite return types are not mergeable when no declared return type is a supertype of all other declared return types.

Counter Example № 163# Schema A
type Query @shareable {
  featured: FeaturedItem
}

union FeaturedItem = Product

type Product @shareable {
  id: ID
}

# Schema B
type Query @shareable {
  featured: Review
}

type Review @shareable {
  id: ID
}

3.2.3.2Field Argument Types Mergeable

Error Code

FIELD_ARGUMENT_TYPES_NOT_MERGEABLE

Severity

ERROR

Formal Specification
  • Let typeNames be the set of all output type names from all source schemas that are not declared as @inaccessible in any schema.
  • For each typeName in typeNames
    • Let types be the set of all types with the typeName from all source schemas that are not declared as @internal.
    • Let fieldNames be the set of all field names from all types that are not declared as @inaccessible in any schema.
    • For each fieldName in fieldNames
      • Let fields be the set of all fields with the fieldName from all types that are not declared as @internal.
      • For each field in fields
        • Let argumentNames be the set of all argument names from all fields.
        • For each argumentName in argumentNames
          • Let arguments be the set of all arguments with the argumentName from all fields.
          • For each pair of argumentA and argumentB in arguments
ArgumentsAreMergeable(argumentA, argumentB)
  1. Let typeA be the type of argumentA
  2. Let typeB be the type of argumentB
  3. SameTypeShape(typeA, typeB) must be true.
Explanatory Text

When multiple schemas define the same field name on the same output type (e.g., User.field), these fields can be merged if their arguments are compatible. Compatibility extends not only to the output field types themselves, but to each argument’s input type as well. The schemas must agree on each argument’s name and have compatible types, so that the composed schema can unify the definitions into a single consistent field specification.

Nullability

Different nullability requirements on arguments are still considered mergeable. For example, if one schema accepts String! and the other accepts String, these schemas can merge; the resulting argument type typically adopts the least restrictive (nullable) version.

Lists Lists of different nullability (e.g., [String!] vs. [String]! vs. [String]) remain mergeable as long as they otherwise refer to the same inner type. Essentially, the same principle of “least restrictive” nullability merges them successfully.

Incompatible Types

If argument types differ on the named type itself – for example, one uses String while the other uses DateTime - this causes a FIELD_ARGUMENT_TYPES_NOT_MERGEABLE error. Similarly, if one schema has [String] but another has [DateTime], they are incompatible.

Example № 164type User {
  field(argument: String): String
}

type User {
  field(argument: String): String
}

Arguments that differ on nullability of an argument type are mergeable.

Example № 165type User {
  field(argument: String!): String
}

type User {
  field(argument: String): String
}
Example № 166type User {
  field(argument: [String!]): String
}

type User {
  field(argument: [String]!): String
}

type User {
  field(argument: [String]): String
}

Arguments are not mergeable if the named types are different in kind or name.

Counter Example № 167type User {
  field(argument: String!): String
}

type User {
  field(argument: DateTime): String
}
Counter Example № 168type User {
  field(argument: [String]): String
}

type User {
  field(argument: [DateTime]): String
}

3.2.3.3Field With Missing Required Arguments

Error Code:

FIELD_WITH_MISSING_REQUIRED_ARGUMENT

Severity:

ERROR

Formal Specification:
  • Let typeNames be the set of all object and interface type names from all source schemas that are not declared as @internal
  • For each typeName in typeNames:
    • Let typeDefinitions be the list of all type definitions from different source schemas with the name typeName.
    • Let fieldNames be the set of all field names from all typeDefinitions that are not declared as @internal.
    • For each fieldName in fieldNames:
      • Let fieldDefinitions be the list of all field definitions from typeDefinitions with the name fieldName.
      • Let requiredArgumentNames be the set of all argument names from fieldDefinitions that have a non-nullable type in at least one definition that does not specify @require
      • For each fieldDefinition in fieldDefinitions:
        • For each requiredArgumentName in requiredArgumentNames:
          • fieldDefinition must have an argument with the name requiredArgumentName that does not specify @require
Explanatory Text:

When merging a field definition across multiple schemas, any argument that is non-null (i.e., “required”) in one schema must appear in all schemas that define that field. In other words, arguments are effectively merged by intersection: if an argument is considered required in any schema, then that same argument must exist in every schema that contributes to the composite definition. If a required argument is missing in one schema, there is no consistent way to define that field across schemas.

If an argument is marked with @require, it is treated as non-required. Consequently, this argument must either be nullable in all other schemas or must also be marked with @require in all other schemas.

Examples

All schemas agree on having a required argument author for the books field:

Example № 169# Schema A
type Query {
  books(author: String!): [Book] @shareable
}

# Schema B
type Query {
  books(author: String!): [Book] @shareable
}

In the following example, the author argument on the books field in Schema A specifies a dependency on the author field in Schema C. The author argument on the books field in Schema B is optional. As a result, the composition succeeds; however, the author argument will not be included in the composite schema.

Example № 170# Schema A
type Collection {
  books(author: String! @require(field: "author")): [Book] @shareable
}

# Schema B
type Collection {
  books(author: String): [Book] @shareable
}

# Schema C
type Collection {
  author: String!
}

In the following counter-example, the author argument is required in one schema but not in the other. This will result in a FIELD_WITH_MISSING_REQUIRED_ARGUMENT error.

Counter Example № 171# Schema A
type Query {
  books(author: String!): [Book] @shareable
}

# Schema B
type Query {
  books: [Book] @shareable
}

In the following counter-example, the author argument on the books field in Schema A specifies a dependency on the author field in Schema C. The author argument on the books field in Schema B is not optional. This will result in a FIELD_WITH_MISSING_REQUIRED_ARGUMENT error.

Counter Example № 172# Schema A
type Collection {
  books(author: String! @require(field: "author")): [Book] @shareable
}

# Schema B
type Collection {
  books(author: String!): [Book] @shareable
}

# Schema C
type Collection {
  author: String!
}

The same reasoning applies when the contributing schemas are @interfaceObject stand-ins for the same interface, rather than ordinary object type declarations. In the following counter-example, source schema B marks minRating with @require. The executor supplies it from Media.rating, which is declared by source schema A. Source schema C instead declares minRating as an ordinary, non-nullable, client-supplied argument on the same field. In source schema B the argument is executor-supplied; in source schema C it is client-supplied. The two declarations are therefore not mergeable, and composition fails with a FIELD_WITH_MISSING_REQUIRED_ARGUMENT error.

Counter Example № 173# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  title: String!
  rating: Int!
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  recommended(minRating: Int! @require(field: "rating")): [Review!]! @shareable
}

type Review {
  id: ID! @shareable
  rating: Int! @shareable
}

# Source Schema C
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  recommended(minRating: Int!): [Review!]! @shareable
}

type Review {
  id: ID! @shareable
  rating: Int! @shareable
}

3.2.4Validate Input Types

3.2.4.1Input Field Default Mismatch

Error Code

INPUT_FIELD_DEFAULT_MISMATCH

Formal Specification
  • Let inputFieldsByName be a map where the key is the name of an input field and the value is a list of input fields from different source schemas from the same type with the same name.
  • For each inputFields in inputFieldsByName:
    • Let defaultValues be a set containing the default values of each input field in inputFields.
    • If the size of defaultValues is greater than 1:
InputFieldsHaveConsistentDefaults(inputFields)
  1. Given each pair of input fields inputFieldA and inputFieldB in inputFields:
    1. If inputFieldA has a default value and inputFieldB has a default value:
      1. If the default value of inputFieldA is not equal to the default value of inputFieldB:
        1. return false
  2. return true
Explanatory Text

Input fields in different source schemas that have the same name are required to have consistent default values. This ensures that there is no ambiguity or inconsistency when merging input fields from different source schemas.

A mismatch in default values for input fields with the same name across different source schemas will result in a schema composition error.

Examples

In the the following example both source schemas have an input field genre with the same default value. This is valid:

Example № 174# Schema A

input BookFilter {
  genre: Genre = FANTASY
}

enum Genre {
  FANTASY
  SCIENCE_FICTION
}

# Schema B
input BookFilter {
  genre: Genre = FANTASY
}

enum Genre {
  FANTASY
  SCIENCE_FICTION
}

If only one of the source schemas defines a default value for a given input field, the composition is still valid:

Example № 175# Schema A

input BookFilter {
  genre: Genre
}

enum Genre {
  FANTASY
  SCIENCE_FICTION
}

# Schema B
input BookFilter {
  genre: Genre = FANTASY
}

enum Genre {
  FANTASY
  SCIENCE_FICTION
}

In the following example both source schemas define an input field minPageCount with different default values. This is invalid:

Counter Example № 176# Schema A

input BookFilter {
  minPageCount: Int = 10
}

# Schema B

input BookFilter {
  minPageCount: Int = 20
}

3.2.4.2Input Field Types mergeable

Error Code

INPUT_FIELD_TYPES_NOT_MERGEABLE

Formal Specification
  • Let fieldsByName be a map of field lists where the key is the name of a field and the value is a list of fields from mergeable input types from different source schemas with the same name.
  • For each fields in fieldsByName:
InputFieldsAreMergeable(fields)
  1. Given each pair of members fieldA and fieldB in fields:
    1. Let typeA be the type of fieldA.
    2. Let typeB be the type of fieldB.
    3. SameTypeShape(typeA, typeB) must be true.
Explanatory Text

The input fields of input objects with the same name must be mergeable. This rule ensures that input objects with the same name in different source schemas have fields that can be merged consistently without conflicts.

Input fields are considered mergeable when they share the same name and have compatible types. The compatibility of types is determined by their structure (e.g., lists), excluding nullability. Mergeable input fields with different nullability are considered mergeable, and the resulting merged field will be the most permissive of the two.

In this example, the field name in AuthorInput has compatible types across source schemas, making them mergeable:

Example № 177input AuthorInput {
  name: String!
}

input AuthorInput {
  name: String
}

The following example shows that fields are mergeable if they have different nullability but the named type is the same and the list structure is the same.

Example № 178input AuthorInput {
  tags: [String!]
}

input AuthorInput {
  tags: [String]!
}

input AuthorInput {
  tags: [String]
}

In this example, the field birthdate on AuthorInput is not mergeable as the field has different named types (String and DateTime) across source schemas:

Counter Example № 179input AuthorInput {
  birthdate: String!
}

input AuthorInput {
  birthdate: DateTime!
}

3.2.4.3Input With Missing Required Fields

Error Code:

INPUT_WITH_MISSING_REQUIRED_FIELDS

Severity:

ERROR

Formal Specification:
  • Let typeNames be the set of all input object types names from all source schemas that are not declared as @inaccessible.
  • For each typeName in typeNames:
    • Let types be the list of all input object types from different source schemas with the name typeName.
    • AreTypesConsistent(types) must be true.
AreTypesConsistent(inputs)
  1. Let requiredFields be the intersection of all field names across all input objects in inputs that are not marked as @inaccessible in any schema and have a non-nullable type in at least one schema.
  2. For each input in inputs:
    1. For each requiredField in requiredFields:
      1. If requiredField is not in input:
        1. Return false
Explanatory Text:

Input types are merged by intersection, meaning that the merged input type will have all fields that are present in all input types with the same name. This rule ensures that input object types with the same name across different schemas share a consistent set of required fields.

Examples

If all schemas define BookFilter with the required field title, the rule is satisfied:

# Schema A
input BookFilter {
  title: String!
  author: String
}

# Schema B
input BookFilter {
  title: String!
  yearPublished: Int
}

If title is required in one source schema but missing in another, this violates the rule:

# Schema A
input BookFilter {
  title: String!
  author: String
}

# Schema B
input BookFilter {
  author: String
  yearPublished: Int
}

In this invalid case, title is mandatory in Schema A but not defined in Schema B, causing inconsistency in required fields across schemas.

3.2.5Validate External Directives

3.2.5.1External Missing on Base

Error Code

EXTERNAL_MISSING_ON_BASE

Severity

ERROR

Formal Specification
  • Let typeNames be the set of all output type names from all source schemas.
  • For each typeName in typeNames
    • Let types be the set of all types with the name typeName from all source schemas.
    • Let fieldNames be the set of all field names from all types in types.
    • For each fieldName in fieldNames
      • Let fields be the set of all fields with the name fieldName from all types in types.
      • Let externalFields be the set of all fields in fields that are marked with @external.
      • Let nonExternalFields be the set of all fields in fields that are not marked with @external.
      • If externalFields is not empty
        • nonExternalFields must not be empty.
Explanatory Text

This rule ensures that any field marked as @external in a source schema is actually defined (non-@external) in at least one other source schema. The @external directive is used to indicate that the field is not usually resolved by the source schema it is declared in, implying it should be resolvable by at least one other source schema.

Here, the name field on Product is defined in source schema A and marked as @external in source schema B, which is valid because there is a base definition in source schema A:

Example № 180# Source Schema A
type Product {
  id: ID
  name: String
}

# Source Schema B
type Product {
  id: ID
  name: String @external
}

In this example, the name field on Product is marked as @external in source schema B but has no non-@external declaration in any other source schema, violating the rule:

Counter Example № 181# Source Schema A
type Product {
  id: ID
}

# Source Schema B
type Product {
  id: ID
  name: String @external
}

3.2.5.2External Type Mismatch

Error Code

EXTERNAL_TYPE_MISMATCH

Severity

ERROR

Formal Specification
  • Let typeNames be the set of all output type names from all source schemas.
  • For each typeName in typeNames
    • Let types be the set of all types with the name typeName from all source schemas.
    • Let fieldNames be the set of all field names from all types in types.
    • For each fieldName in fieldNames
      • Let fields be the set of all fields with the name fieldName from all types in types.
      • Let externalFields be the set of all fields in fields that are marked with @external.
      • Let nonExternalFields be the set of all fields in fields that are not marked with @external.
      • For each externalField in externalFields
        • The type of externalField must strictly equal all types of nonExternalFields.
Explanatory Text

This rule ensures that a field marked as @external has a return type compatible with the corresponding field defined in other source schemas. Fields with the same name must represent the same data type to maintain schema consistency

Here, the @external field name has the same return type (String) as the base field definition, satisfying the rule:

Example № 182# Source Schema A
type Product {
  name: String
}

# Source Schema B
type Product {
  name: String @external
}

In this example, the @external field name has a return type of ProductName that doesn’t match the base field’s return type String, violating the rule:

Counter Example № 183# Source Schema A
type Product {
  name: String
}

# Source Schema B
type Product {
  name: ProductName @external
}

3.2.6Validate Override Directives

3.2.6.1Override Source Has Override

Error Code

OVERRIDE_SOURCE_HAS_OVERRIDE

Severity

ERROR

Formal Specification
  • Let schemas be the set of all source schemas to be composed.
  • Let implementationEdges be MergeInterfaceImplementations(schemas).
  • Let overriddenDeclarations be an empty set.
  • Let groupedTypes be a map grouping all object types from schemas by their type name.
  • For each typeGroup in groupedTypes:
    • Let types be the set of object types in typeGroup.
    • Let groupedFields be a map grouping every field across all types by their field name.
    • For each fieldGroup in groupedFields:
      • Let fields be the set of field definitions in fieldGroup.
      • Let overrides be the set of fields in fields annotated with @override.
      • overrides must contain at most one field.
      • For each override in overrides:
        • Let targets be CollectOverrideTargets(override, schemas, implementationEdges).
        • For each target in targets:
          • target must not be annotated with @override.
          • overriddenDeclarations must not contain target.
          • Add target to overriddenDeclarations.
Explanatory Text

A field marked with @override signifies that its ownership is being taken over by another schema. If multiple schemas try to override the same field, or if the ownership chain loops back on itself, the composed schema has more than one @override for a single field. This creates ambiguity about which schema ultimately owns that field.

Hence, only one @override may ever apply to a particular field across all source schemas. Attempting multiple overrides, or forming any cycle of overrides for the same field, triggers the OVERRIDE_SOURCE_HAS_OVERRIDE error.

@override is also legal on a field declared by an @interfaceObject stand-in. It drops the field from the implementing types and stand-ins that the named source schema declares. Because the named schema’s own stand-in loses the field as well, the projected implementation can move from one schema to another. A projected field is not itself a source-schema declaration, so @override cannot take it from an implementing type. If a direct declaration remains alongside a projected implementation, all eligible declarations must satisfy Invalid Projected Field Sharing. An @override that names a schema without a matching field drops nothing.

Examples

In this scenario, Bill.amount is originally owned by Schema A but is overridden in Schema B. No other schema further attempts to override the same field, so the composition is valid.

Example № 184# Source Schema A
type Bill {
  id: ID!
  amount: Int
}

# Source Schema B
type Bill {
  id: ID!
  amount: Int @override(from: "SchemaA")
}

Here, Schema A overrides Bill.amount from Schema B, while Schema B also overrides the same field from Schema A. This circular override makes it impossible to discern a single “owner” of the field Bill.amount, raising an OVERRIDE_SOURCE_HAS_OVERRIDE error.

Counter Example № 185# Source Schema A (named "SchemaA")
type Bill {
  id: ID!
  amount: Int @override(from: "SchemaB")
}

# Source Schema B (named "SchemaB")
type Bill {
  id: ID!
  amount: Int @override(from: "SchemaA")
}

In this case, the same field Bill.amount is overridden successively by A, then B, then C. Tracing these overrides forms a cycle (A → B → C → A). This again produces an OVERRIDE_SOURCE_HAS_OVERRIDE error.

Counter Example № 186# Source Schema A (named "A")
type Bill {
  id: ID!
  amount: Int @override(from: "B")
}

# Source Schema B (named "B")
type Bill {
  id: ID!
  amount: Int @override(from: "C")
}

# Source Schema C (named "C")
type Bill {
  id: ID!
  amount: Int @override(from: "A")
}

In the following counter-example, the field Bill.amount is overridden by multiple schemas. The overrides do not form a cycle, hence there are multiple overrides for the same field, triggering an OVERRIDE_SOURCE_HAS_OVERRIDE error.

Counter Example № 187# Source Schema A
type Bill {
  id: ID!
  amount: Int @override(from: "SchemaC")
}

# Source Schema B
type Bill {
  id: ID!
  amount: Int @override(from: "SchemaC")
}

# Source Schema C
type Bill {
  id: ID!
  amount: Int
}

In this example, the implementation projected for Media.reviews moves from the original “Reviews” schema to the new “Reviews2” schema. The “Reviews2” stand-in overrides the “Reviews” stand-in field, so only the “Reviews2” declaration supplies the projected implementation; no implementing type needs to change.

Example № 188# The "Catalog" schema:
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

# The original "Reviews" schema:
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  id: ID! @shareable
  rating: Int! @shareable
}

# The new "Reviews2" schema:
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]! @override(from: "Reviews")
}

type Review {
  id: ID! @shareable
  rating: Int! @shareable
}

The same restriction applies across interface boundaries. In this counter-example, source schema C’s override targets source schema A’s PhysicalProduct.price, which itself overrides source schema B. Composition rejects the chain before dropping either declaration, regardless of the order in which the interfaces are processed.

Counter Example № 189# Source Schema D
interface Product @key(fields: "id") {
  id: ID!
}

interface PhysicalProduct implements Product @key(fields: "id") {
  id: ID!
}

type Chair implements PhysicalProduct & Product @key(fields: "id") {
  id: ID!
}

# Source Schema A (named "A")
type PhysicalProduct @interfaceObject @key(fields: "id") {
  id: ID!
  price: Float @override(from: "B")
}

# Source Schema B (named "B")
type PhysicalProduct @interfaceObject @key(fields: "id") {
  id: ID!
  price: Float
}

# Source Schema C
type Product @interfaceObject @key(fields: "id") {
  id: ID!
  price: Float @override(from: "A")
}

3.2.7Validate Shareable Directives

3.2.7.1Invalid Field Sharing

Error Code

INVALID_FIELD_SHARING

Severity

ERROR

Formal Specification
  • Let schemas be the source schemas.
  • Let overriddenDeclarations be CollectOverriddenDeclarations(schemas).
  • Let typeNames be the names of object types in schemas not annotated with @internal.
  • For each typeName in typeNames:
    • Let types be the object types with that name not annotated with @internal.
    • Let declarations be the fields on types, excluding fields annotated with @internal or @external and fields in overriddenDeclarations.
    • For each group of declarations with the same field name:
      • If the group contains more than one declaration:
Explanatory Text

A field in a federated GraphQL schema may be marked @shareable, indicating that the same field can be resolved by multiple schemas without conflict. When a field is not marked as @shareable (sometimes called “non-shareable”), it cannot be provided by more than one schema.

Field definitions marked as @external and overridden fields are excluded when validating whether a field is shareable. These annotations indicate specific cases where field ownership lies with another schema or has been replaced.

Examples

In this example, the User type field fullName is marked as shareable in both schemas, allowing them to serve consistent data for that field without conflict.

Example № 190# Schema A
type User @key(fields: "id") {
  id: ID!
  username: String
  fullName: String @shareable
}

# Schema B
type User @key(fields: "id") {
  id: ID!
  fullName: String @shareable
  email: String
}

In the following example, User.fullName is overridden in one schema and therefore the field can be defined in the other schema without being marked as @shareable.

Example № 191# Schema A
type User @key(fields: "id") {
  id: ID!
  fullName: String @override(from: "B")
}

# Schema B
type User @key(fields: "id") {
  id: ID!
  fullName: String
}

In the following example, User.fullName is marked as @external in one schema and therefore the field can be defined in the other schema without being marked as @shareable.

Example № 192# Schema A
type User @key(fields: "id") {
  id: ID!
  fullName: String @external
}

# Schema B
type User @key(fields: "id") {
  id: ID!
  fullName: String
}

In the following counter-example, User.fullName is non-shareable but is defined and resolved by two different schemas, resulting in an INVALID_FIELD_SHARING error.

Counter Example № 193# Schema A
type User @key(fields: "id") {
  id: ID!
  fullName: String
}

# Schema B
type User @key(fields: "id") {
  id: ID!
  fullName: String
}

3.2.8Validate Interface Object Directives

3.2.8.1Interface Object No Interface

Error Code

INTERFACE_OBJECT_NO_INTERFACE

Severity

ERROR

Formal Specification
  • Let schemas be the set of all source schemas.
  • Let typeNames be the set of all type names for which at least one schema in schemas declares an object type annotated with @interfaceObject.
  • For each typeName in typeNames:
    • Let definitions be the set of all type definitions named typeName across schemas.
    • Let interfaceDefinitions be the subset of definitions that are interface types.
    • interfaceDefinitions must not be empty.
Explanatory Text

A stand-in binds to the interface of the same name. No source schema that declares a stand-in references any other. At least one source schema must define that name as an interface. If every source schema that declares the type name uses @interfaceObject, and none defines it as an interface, the stand-in has no interface to bind to and composition fails with an INTERFACE_OBJECT_NO_INTERFACE error.

Examples

In this example, source schema A defines Media as a real interface, so source schema B’s stand-in is valid.

Example № 194# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

type Book implements Media {
  id: ID!
  title: String!
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  id: ID!
  rating: Int!
}

In the following counter-example, both source schema B and source schema C declare Media as an @interfaceObject stand-in, but no source schema defines Media as an interface. Neither stand-in has an interface to bind to, so composition fails with an INTERFACE_OBJECT_NO_INTERFACE error.

Counter Example № 195# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  id: ID!
  rating: Int!
}

# Source Schema C
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  averageRating: Float!
}

3.2.8.2Interface Object Key Mismatch

Error Code

INTERFACE_OBJECT_KEY_MISMATCH

Severity

ERROR

Formal Specification
  • Let schemas be the set of all source schemas.
  • Let standIns be the set of all object types across schemas annotated with @interfaceObject.
  • For each standIn in standIns:
    • Let typeName be the name of standIn.
    • Let interfaceDefinitions be the set of all interface types named typeName across schemas.
    • Let interfaceKeys be the set of field selection sets declared by @key directives on the types in interfaceDefinitions.
    • interfaceKeys must not be empty.
    • For each @key directive keyDirective on standIn:
      • Let fieldSet be the selection set of the fields argument of keyDirective.
      • interfaceKeys must contain an entry that selects the same fields as fieldSet.
Explanatory Text

An interface that has a stand-in must declare at least one key with @key. The stand-in must key on one of the keys of the interface. Each @key on the stand-in must select the same fields as a @key declared on the interface by at least one interface-defining schema. The comparison is structural; the order of the fields and their formatting do not matter.

Examples

In this example, the Media interface declares two keys, id and sku. The stand-in in source schema B keys on sku alone. Because sku is one of the keys of the interface, this is valid even though the stand-in does not repeat the id key.

Example № 196# Source Schema A
interface Media @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String!
  title: String!
}

# Source Schema B
type Media @interfaceObject @key(fields: "sku") {
  sku: String!
  reviews: [Review!]!
}

type Review {
  id: ID!
  rating: Int!
}

In the following counter-example, the stand-in in source schema B keys on upc, but the Media interface declares no key with that field. This results in an INTERFACE_OBJECT_KEY_MISMATCH error.

Counter Example № 197# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

# Source Schema B
type Media @interfaceObject @key(fields: "upc") {
  upc: String!
  reviews: [Review!]!
}

type Review {
  id: ID!
  rating: Int!
}

In the following counter-example, the Media interface declares no @key at all. A stand-in for Media can therefore not declare a matching key, and composition fails.

Counter Example № 198# Source Schema A
interface Media {
  id: ID!
  title: String!
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  id: ID!
  rating: Int!
}

3.3Merge

During this stage, all definitions from each source schema are combined into a single schema. This section defines the rules for merging schema definitions. The goal is to create a composite schema that includes all type system members from each source schema that are publicly accessible.

MergeSchemas(schemas)
  1. Let mergedSchema be an empty schema.
  2. Let implementationEdges be the result of MergeInterfaceImplementations(schemas).
  3. Let overriddenDeclarations be the result of CollectOverriddenDeclarations(schemas).
  4. Record implementationEdges, including their declared or derived provenance, and overriddenDeclarations on mergedSchema.
  5. Let memberNames be the set of all scalar, object, interface, union, enum and input type names in schemas.
  6. During merging, interpret every reference to a stand-in object type as a reference to its corresponding interface. Preserve the original source-local type references in execution metadata.
  7. For each memberName in memberNames:
    1. Let types be the set of all types named memberName across all source schemas.
    2. Let mergedType be the result of MergeTypes(types, overriddenDeclarations).
    3. If mergedType is not null:
      1. Add mergedType to mergedSchema.
  8. For each object or interface type type in mergedSchema:
    1. Set its implemented interfaces to the interface types present in mergedSchema named by the pairs (the name of type, interfaceName) in implementationEdges.
  9. Perform ProjectInterfaceObjectFields(schemas, mergedSchema).
  10. Return mergedSchema.
MergeTypes(types, overriddenDeclarations)
  1. Let firstType be the first type in types.
  2. If any type in types is an interface type:
    1. Assert: Every type in types is either an interface type or an object type annotated with @interfaceObject.
    2. Return the result of MergeInterfaceTypes(types, overriddenDeclarations).
  3. Let kind be the kind of firstType.
  4. Assert: All types in types have the same kind, and none is annotated with @interfaceObject.
  5. If kind is SCALAR:
    1. Return the result of MergeScalarTypes(types).
  6. If kind is ENUM:
    1. Return the result of MergeEnumTypes(types).
  7. If kind is UNION:
    1. Return the result of MergeUnionTypes(types).
  8. If kind is INPUT_OBJECT:
    1. Return the result of MergeInputTypes(types).
  9. If kind is OBJECT:
    1. Return the result of MergeObjectTypes(types, overriddenDeclarations).

3.3.1Merge Scalar Types

Formal Specification
MergeScalarTypes(scalars)
  1. If any scalar in scalars is marked with @inaccessible
    1. Return null
  2. Let firstScalar be the first scalar in scalars.
  3. Let description be the description of firstScalar.
  4. For each scalar in scalars:
    1. If description is null:
      1. Set description to the description of scalar.
  5. Return a new scalar type with the name of firstScalar and description of description.
Explanatory Text

MergeScalarTypes(scalars) merges multiple scalar definitions that share the same name into a single scalar type. It filters out scalars marked with @inaccessible and unifies descriptions so that the final type retains the first available non-null description.

Inaccessible Scalars

If any scalar is labeled with @inaccessible, the merge immediately returns null. A scalar that cannot be exposed to consumers renders the entire type unusable.

Combining Descriptions

The final description is determined by the first non-null description found in the list of scalars. If no descriptions are found, the final description is null.

Examples

Here, two Date scalar types from different schemas are merged into a single composed Date scalar type.

Example № 199# Schema A

scalar Date

# Schema B

"A scalar representing a calendar date."
scalar Date

# Composed Result

"A scalar representing a calendar date."
scalar Date

3.3.2Merge Interface Types

Formal Specification
MergeInterfaceTypes(types, overriddenDeclarations)
  1. Remove all types marked with @internal from types.
  2. If types is empty:
    1. Return null.
  3. If any type in types is marked with @inaccessible
    1. Return null
  4. Let firstType be the first type in types.
  5. Let typeName be the name of firstType.
  6. Let description be the description of firstType.
  7. Let mergedFields be an empty set.
  8. For each type in types:
    1. If description is null:
      1. Set description to the description of type.
  9. Let fieldNames be the set of all field names in types.
  10. For each fieldName in fieldNames:
    1. Let fields be the set of fields with the name fieldName in types, excluding fields marked with @internal.
    2. If any field in fields is marked with @inaccessible:
      1. Continue.
    3. Remove declarations in overriddenDeclarations from fields.
    4. If fields is empty:
      1. Continue.
    5. Let mergedField be the result of MergeOutputFields(fields).
    6. If mergedField is not null:
      1. Add mergedField to mergedFields.
  11. Return a new interface type with the name of typeName, description of description, and fields of mergedFields.
Explanatory Text

MergeInterfaceTypes(types, overriddenDeclarations) unifies interface definitions and their stand-ins sharing the same name into a single composed interface type. It excludes internal types before merging. If any remaining type is marked @inaccessible, the merge immediately returns null, preventing inclusion of that interface in the final schema.

Inaccessible Interfaces

A type marked @inaccessible disqualifies the entire merge, ensuring no references to inaccessible types appear in the final schema.

Combining Descriptions

Among the valid interfaces, the description is taken from the first non-null description encountered. If all interfaces lack a description, the resulting interface has none.

Merging Fields

Each interface and stand-in contributes its fields, excluding declarations targeted by an override. Fields that share the same name are reconciled via MergeOutputFields(fields). This ensures any differences in type, nullability, or other constraints are resolved before appearing in the final interface.

By applying these steps, MergeInterfaceTypes(types, overriddenDeclarations) produces a coherent interface type definition that reflects the fields from all compatible sources while adhering to accessibility constraints.

Examples

Here, two Product interface types from different schemas are merged into a single composed Product interface type.

Example № 200# Schema A

interface Product {
  id: ID!
  name: String
}

# Schema B

interface Product {
  id: ID!
  createdAt: String
}

# Composed Result

interface Product {
  id: ID!
  name: String
  createdAt: String
}

In this example, the Product interface type from two schemas is merged. The id field is shared across both schemas, while name and createdAt fields are contributed by the individual source schemas. The resulting composed type includes all fields.

The following example shows how the description is retained when merging interface types:

Example № 201# Schema A

"""
First description
"""
interface Product {
  id: ID!
}

# Schema B

"""
Second description
"""
interface Product {
  id: ID!
}

# Composed Result

"""
First description
"""
interface Product {
  id: ID!
}

3.3.3Merge Enum Types

Formal Specification
MergeEnumTypes(enums)
  1. If any enum in enums is marked with @inaccessible
    1. Return null
  2. Let firstEnum be the first enum in enums.
  3. If enums contains only one enum
    1. Return a new enum type with the name of firstEnum, description of firstEnum, and enum values of firstEnum excluding any marked with @inaccessible.
  4. Let typeName be the name of firstEnum.
  5. Let description be the first non empty description of any enum in enums.
  6. Let mergedValues be an empty set.
  7. Let valueNames be the set of all enum value names in enums.
  8. For each valueName in valueNames:
    1. Let values be the set of enum values with the name valueName in enums.
    2. Let mergedValue be the result of MergeEnumValues(values).
    3. If mergedValue is not null:
      1. Add mergedValue to mergedValues.
  9. Return a new enum type with the name of typeName, description of description, and enum values of mergedValues.
MergeEnumValues(enumValues)
  1. If any enumValue in enumValues is marked with @inaccessible
    1. Return null
  2. Let name be the name of the first enumValue in enumValues.
  3. Let description be the first non empty description of any enumValue in enumValues.
  4. Return a new enum value with the name of name and description of description.
Explanatory Text

MergeEnumTypes(enums) consolidates multiple enum definitions (all sharing the same name) into one final enum type, while filtering out any parts marked with @inaccessible. If an entire enum is inaccessible, the merge returns null.

Inaccessible Enums

If any enum in the input set is marked @inaccessible, the entire merge operation is invalid. The algorithm immediately returns null, since that type cannot appear in the composed schema.

Single vs. Multiple Enum Definitions

When only one enum definition is present (after removing any inaccessible ones), it is used as is, except that any values marked with @inaccessible are excluded.

However, if an enum appears in multiple schemas, the enums must match exactly in their values and structure unless some values are excluded using the @inaccessible directive. This behavior is enforced by prior validation but is important to note as it determines how mismatched enums are handled.

Combining Descriptions

The first non-null description encountered among the enums is used for the final definition. If no definitions supply a description, the merged enum will have none.

Examples

Here, two Status enums from different schemas are merged into a single composed Status enum. The enums are identical, so the composed enum exactly matches the source enums.

Example № 202# Schema A

enum Status {
  ACTIVE
  INACTIVE
}

# Schema B

enum Status {
  ACTIVE
  INACTIVE
}

# Composed Result

enum Status {
  ACTIVE
  INACTIVE
}

If the enums differ in their values, the source schemas must define their unique values as @inaccessible to exclude them from the composed enum.

Example № 203# Schema A

enum Status {
  ACTIVE @inaccessible
  INACTIVE
}

# Schema B

enum Status {
  PENDING @inaccessible
  INACTIVE
}

# Composed Result

enum Status {
  INACTIVE
}

3.3.4Merge Union Types

Formal Specification
MergeUnionTypes(unions)
  1. If any union in unions is marked with @inaccessible
    1. Return null
  2. Let firstUnion be the first union in unions.
  3. Let name be the name of firstUnion.
  4. Let description be the description of firstUnion.
  5. Let possibleTypes be an empty set.
  6. For each union in unions:
    1. If description is null:
      1. Set description to the description of union.
    2. For each possibleType in the possible types of union:
      1. If possibleType is not marked with @inaccessible or @internal:
        1. Add possibleType to possibleTypes.
  7. Return a new union with the name of name, description of description, and possible types of possibleTypes.
Explanatory Text

MergeUnionTypes(unions) aggregates multiple union type definitions that share the same name into one unified union type. This process skips any union marked with @inaccessible and excludes possible types marked with @inaccessible or @internal.

Inaccessible Unions

If any union in the input list is marked @inaccessible, the merged result must be null and cannot appear in the final schema.

Combining Descriptions

The first non-empty description that is found is used as the description for the merged union. If no descriptions are found, the merged union will have no description.

Combining Possible Types

Each union’s possible types are considered in turn. Only those that are not marked @internal or @inaccessible are included in the final composed union. This preserves the valid types from all sources while systematically filtering out anything inaccessible or intended for internal use only.

In case there are no possible types left after filtering, the merged union is considered @inaccessible and cannot appear in the final schema.

Examples

Here, two SearchResult union types from different schemas are merged into a single composed SearchResult type.

Example № 204# Schema A

union SearchResult = Product | Order

# Schema B

union SearchResult = User | Order

# Composed Result

union SearchResult = Product | Order | User

In this example, the SearchResult union type from two schemas is merged. The Order type is shared across both schemas, while Product and User types are contributed by the individual source schemas. The resulting composed type includes all valid possible types.

Another example shows how @inaccessible on a possible affects the merge:

Example № 205# Schema A

union SearchResult = Product | Order

type Product @inaccessible {
  id: ID!
}

# Schema B

union SearchResult = User | Order

# Composed Result

union SearchResult = Order | User

In this case, the Product type is marked with @inaccessible in the first schema. As a result, the Product type is excluded from the composed SearchResult

3.3.5Merge Input Types

Formal Specification
MergeInputTypes(types)
  1. If any type in types is marked with @inaccessible
    1. Return null
  2. Let firstType be the first type in types.
  3. Let typeName be the name of firstType.
  4. Let description be the description of firstType.
  5. Let mergedFields be an empty set.
  6. For each type in types:
    1. If description is null:
      1. Set description to the description of type.
  7. Let fieldNames be the set of all field names in types.
  8. For each fieldName in fieldNames:
    1. Let fieldDefinitions be the set of fields with the name fieldName in types.
    2. If length of fieldDefinitions is not equal to the length of types:
      1. Continue
    3. If any field in fieldDefinitions is marked with @inaccessible
      1. Continue
    4. Let mergedField be the result of MergeInputFields(fieldDefinitions).
    5. If mergedField is not null:
      1. Add mergedField to fields.
  9. If fields is empty:
    1. Return null
  10. Return a new input type with the name of typeName, description of description, and fields of fields.
Explanatory Text

The MergeInputTypes(types) algorithm produces a single input type definition by unifying multiple input types that share the same name. Each of these input types may come from different sources, yet must align into one coherent definition. Any type marked @inaccessible disqualifies the entire merge result from inclusion in the composed schema.

Inaccessible Types

If an input type is annotated with @inaccessible, the algorithm immediately returns null. Including an inaccessible type would mean exposing a field that’s not allowed in the composed schema.

Combining Descriptions

The first non-null description encountered is used for the final input type. If no such description exists among the source types, the resulting input type definition has no description.

Merging Fields

After filtering out inaccessible types, the algorithm merges each input field name found across the remaining types. For each field, MergeInputFields(fields) is called to reconcile differences in type, nullability, default values, etc.. If a merged field ends up being null - for instance, because one of its underlying definitions was inaccessible – that field is not included in the final definition. The end result is a single input type that correctly unifies every compatible field from the various sources.

After filtering out inaccessible types, the algorithm takes the intersection of the field names across the remaining types – only those fields that appear in every source definition are eligible. For each eligible field, it invokes MergeInputFields(fieldsForName) to reconcile differences in type, nullability, default values, etc. The end result is a single input type that correctly unifies every compatible field that appears in all source types.

Examples

Here, two OrderInput input types from different schemas are merged into a single composed OrderInput type. Notice that only the fields present in both schemas are included.

Example № 206# Schema A

input OrderInput {
  id: ID!
  description: String
}

# Schema B

input OrderInput {
  id: ID!
  total: Float
}

# Composed Result

input OrderInput {
  id: ID!
}

Although description appears in Schema A and total appears in Schema B, neither field is defined in both schemas; therefore, only id remains.

Another example demonstrates preserving descriptions during merging:

Example № 207# Schema A

"""
First Description
"""
input OrderInput {
  id: ID!
}

# Schema B

"""
Second Description
"""
input OrderInput {
  id: ID!
}

# Composed Result

"""
First Description
"""
input OrderInput {
  id: ID!
}

In this case, the description from the first schema is retained, while the fields are merged from both schemas to create the final OrderInput type.

3.3.6Merge Object Types

Formal Specification
MergeObjectTypes(types, overriddenDeclarations)
  1. If any type in types is marked with @inaccessible
    1. Return null
  2. Remove all types marked with @internal from types.
  3. If types is empty:
    1. Return null
  4. Let firstType be the first type in types.
  5. Let typeName be the name of firstType.
  6. Let description be the description of firstType.
  7. Let mergedFields be an empty set.
  8. For each type in types:
    1. If description is null:
      1. Set description to the description of type.
  9. Let fieldNames be the set of all field names in types.
  10. For each fieldName in fieldNames:
    1. Let fields be the set of fields with the name fieldName in types, excluding fields marked with @internal.
    2. If any field in fields is marked with @inaccessible:
      1. Continue.
    3. Remove declarations in overriddenDeclarations from fields.
    4. If fields is empty:
      1. Continue.
    5. Let mergedField be the result of MergeOutputFields(fields).
    6. If mergedField is not null:
      1. Add mergedField to mergedFields.
  11. Return a new object type with the name of typeName, description of description, fields of mergedFields.
Explanatory Text

The MergeObjectTypes(types, overriddenDeclarations) algorithm combines multiple object type definitions (all sharing the same name) into a single composed type. It processes each candidate type, discarding any that are inaccessible or internal, and then unifies their descriptions and fields.

Inaccessible Types

If an object type is marked with @inaccessible, the entire merged result must be null; we cannot include that type in the composed schema. Inaccessible types are disqualified at the outset.

Internal Types

Any type marked with @internal is removed from consideration before merging begins. None of its fields or descriptions will factor into the final composed type.

Combining Descriptions

The first non-null description encountered is used for the final object type’s description. If no non-null description is found, the resulting object type simply has no description.

Merging Fields

All remaining object types contribute their fields, excluding declarations targeted by an override. The algorithm gathers every field name across these types, then calls MergeOutputFields(fields) for each name to reconcile any differences. If MergeOutputFields(fields) returns null (for instance, because a field is marked @inaccessible), that field is excluded from the final object type. The result is a unified set of fields that reflects each source definition while maintaining compatibility across them.

Examples

Here, two Product object types from different schemas are merged into a single composed Product type.

Example № 208# Schema A

type Product @key(fields: "id") {
  id: ID!
  name: String
}

# Schema B

type Product @key(fields: "id") {
  id: ID!
  price: Int
}

# Composed Result

type Product {
  id: ID!
  name: String
  price: Int
}

In this example, the Product type from two schemas is merged. The id field is shared across both schemas, while name and price fields are contributed by the individual source schemas. The resulting composed type includes all fields.

Another example demonstrates preserving descriptions during merging:

Example № 209# Schema A

"""
First Description
"""
type Order @key(fields: "id") {
  id: ID!
}

# Schema B

"""
Second Description
"""
type Order @key(fields: "id") {
  id: ID!
  total: Float
}

# Composed Result

"""
First Description
"""
type Order {
  id: ID!
  total: Float
}

In this case, the description from the first schema is retained, while the fields are merged from both schemas to create the final Order type.

In the following example, one of the Product types is marked with @internal. All its fields are excluded from the composed type.

Example № 210# Schema A

type Product @key(fields: "id") {
  id: ID!
  name: String
}

# Schema B

type Product @key(fields: "id") @internal {
  id: ID!
  price: Int
}

# Composed Result

type Product {
  id: ID!
  name: String
}

3.3.7Merge Interface Implementations

Formal Specification
MergeInterfaceImplementations(schemas)
  1. Let edges be an empty set of (type, interface) pairs.
  2. For each schema in schemas:
    1. Let typeDefinitions be the set of all object and interface type definitions in schema that are not marked with @internal.
    2. For each typeDefinition in typeDefinitions:
      1. Let typeName be the name of typeDefinition.
      2. Let interfaceNames be the set of names of the interfaces that typeDefinition declares as implemented.
      3. For each interfaceName in interfaceNames:
        1. Add the pair (typeName, interfaceName) to edges, recording schema as a source of that declared edge.
  3. Return the result of CloseImplementsEdges(edges).

CloseImplementsEdges(edges):

Completes edges so that implementation is transitive: whenever type implements interface, and interface itself implements parentInterface, the pair (type, parentInterface) is added to the result.

  • Let closedEdges be a copy of edges.
  • Let worklist be a copy of edges.
  • While worklist is not empty:
    • Remove one pair (type, interface) from worklist.
    • For each pair (interface, parentInterface) in closedEdges:
      • If the pair (type, parentInterface) is not in closedEdges:
        • Add the pair (type, parentInterface) to closedEdges, recording it as derived through (type, interface) and (interface, parentInterface).
        • Add the pair (type, parentInterface) to worklist.
  • Return closedEdges.
Explanatory Text

MergeInterfaceImplementations(schemas) computes the complete implements relation for the composite schema: which object and interface types implement which interfaces. It combines every source schema’s local declarations and completes the result so that implementation is always transitive. MergeSchemas(schemas) uses its result to construct every merged object and interface type’s implements clause. Every post-merge rule that reasons about interface implementation uses it too.

Combining Declared Implementations

Every implements relationship declared on an object or interface type that is not internal contributes one pair to edges. A type need not declare the same interfaces consistently across every source schema that defines it. The pair that any single schema contributes is enough to make that implementation part of the composite schema. This mirrors how fields and descriptions are combined elsewhere during merging. Each source schema contributes a partial view, and composition unions them.

Completing the Hierarchy

The GraphQL specification requires that a type transitively implement every interface implemented by any interface it implements. For example, if PhysicalProduct implements Product, every type that implements PhysicalProduct must also declare that it implements Product. Each source schema declares only its local portion of the hierarchy. This obligation therefore often spans schema boundaries that no individual source schema can satisfy on its own. CloseImplementsEdges(edges) closes the relation over this rule. It adds the missing edges automatically, so the composite schema, taken as a whole, stays valid GraphQL.

Partial Views Are Expected

A source schema that declares PhysicalProduct implements Product need not reference every other interface layered onto the same hierarchy elsewhere. A source schema that declares Chair implements PhysicalProduct need not define Product. Neither schema is incomplete or incorrect on its own; only the composite schema must reflect the full hierarchy. Partial, schema-local views of a shared interface hierarchy are expected. MergeInterfaceImplementations(schemas) reconciles them into a single, transitively closed relation.

Ordering Within Composition

MergeInterfaceImplementations(schemas) runs before type merging so that hierarchy-wide overrides can be collected before field signatures are merged. MergeSchemas(schemas) attaches the completed implements clauses after merging the types and before projecting stand-in fields. Projection and every post-merge validation therefore use the same complete relation.

Post-merge rules such as INTERFACE_FIELD_NO_IMPLEMENTATION and IMPLEMENTED_BY_INACCESSIBLE depend on it because both are defined in terms of “the set of interfaces implemented by type” in the merged schema. Neither rule changes to accommodate this algorithm. They evaluate against the complete relation instead of whatever subset of it a single source schema happened to declare. They continue to gate contract completeness exactly as before. A field that a type carries only to satisfy an interface reached through closure is held to the same standard as one reached through a direct declaration.

Note Source schemas hold only partial views of the hierarchy. The distributed executor must not assume that a value’s originating source schema defines every interface the composite schema records for that value. The distributed executor resolves abstract-type membership against the composite schema. This includes, for example, which concrete type backs a value, for __typename or a type condition. The executor rewrites any type condition it sends to a source schema into that schema’s own local type vocabulary. It never sends a source schema a type condition naming a type that source schema does not define.
Note The union alone can produce a schema that violates the GraphQL specification’s transitive implementation rule. This can happen because a type may inherit an interface only through an intermediate interface defined in a different source schema. Completing the transitive closure automatically fixes this. It lets source schemas with correct but partial views of a shared hierarchy compose successfully, without any one of them needing full knowledge of it.
Examples

In this example, two source schemas each declare one interface on the shared Chair type. Neither interface is related to the other, so composition unions the two declarations.

Example № 211# Source Schema A
interface Product {
  id: ID!
}

type Chair implements Product @key(fields: "id") {
  id: ID!
  legs: Int
}

# Source Schema B
interface Searchable {
  score: Float
}

type Chair implements Searchable @key(fields: "id") {
  id: ID!
  score: Float
}

# Composite Schema
interface Product {
  id: ID!
}

interface Searchable {
  score: Float
}

type Chair implements Product & Searchable {
  id: ID!
  legs: Int
  score: Float
}

In the following example, source schema A declares that PhysicalProduct implements Product, while source schema B declares PhysicalProduct again, without that relationship, and separately declares that Chair implements PhysicalProduct. Source schema B never mentions Product.

Example № 212# Source Schema A
interface Product {
  id: ID!
}

interface PhysicalProduct implements Product {
  id: ID!
  weight: Int
}

# Source Schema B
interface PhysicalProduct {
  id: ID!
  weight: Int
}

type Chair implements PhysicalProduct {
  id: ID!
  weight: Int
  legs: Int
}

# Composite Schema
interface Product {
  id: ID!
}

interface PhysicalProduct implements Product {
  id: ID!
  weight: Int
}

type Chair implements PhysicalProduct & Product {
  id: ID!
  weight: Int
  legs: Int
}

Composing PhysicalProduct contributes the pair (PhysicalProduct, Product), declared directly by source schema A. Composing Chair contributes the pair (Chair, PhysicalProduct), declared directly by source schema B. CloseImplementsEdges(edges) then finds that Chair implements PhysicalProduct, and PhysicalProduct implements Product, and adds the pair (Chair, Product) to the result even though no source schema declared it. This derived edge lets the composed Chair type explicitly implement Product, as GraphQL requires. It is recorded as derived, rather than declared, so that tooling can explain why Chair implements an interface that no single source schema named.

3.3.8Project Interface Object Fields

Formal Specification
ProjectInterfaceObjectFields(schemas, mergedSchema)
  1. Assert: The implements relation of mergedSchema is complete (see Merge Interface Implementations).
  2. Let overriddenDeclarations be the set recorded on mergedSchema by MergeSchemas(schemas).
  3. For each interface type interface in mergedSchema:
    1. Let ownDeclarations be the result of ContractFieldDeclarations(interface, schemas).
    2. Let fieldNames be the names of fields in ownDeclarations.
    3. For each interface type ancestor that interface implements:
      1. Add the name of each field in ContributedFields(ancestor, schemas) to fieldNames.
    4. For each fieldName in fieldNames:
      1. Let projectedDeclarations be the result of ContributingDeclarations(interface, fieldName, schemas, mergedSchema).
      2. If projectedDeclarations is empty:
        1. Continue.
      3. Let declarations be the union of projectedDeclarations and the fields named fieldName in ownDeclarations.
      4. Let mergedField be the result of MergeProjectedOutputFields(declarations).
      5. If mergedField is null:
        1. Remove the field named fieldName from interface, if present.
      6. Otherwise:
        1. Set the field named fieldName on interface to mergedField.
        2. Record declarations as the declarations used to construct that interface field.
  4. For each object type objectType in mergedSchema:
    1. Let projectedFieldNames be an empty set.
    2. For each interface type interface that objectType implements:
      1. Add the name of each field in ContributedFields(interface, schemas) to projectedFieldNames.
    3. For each fieldName in projectedFieldNames:
      1. Let projectedDeclarations be the result of ContributingDeclarations(objectType, fieldName, schemas, mergedSchema).
      2. Assert: projectedDeclarations is not empty.
      3. Let localDeclarations be the fields named fieldName on object types named the name of objectType across schemas, excluding types marked with @internal and fields marked with @internal.
      4. If any declaration in localDeclarations is marked with @inaccessible:
        1. Continue. The field remains absent from objectType.
      5. Let directDeclarations be the subset of localDeclarations for which IsEligibleOwnerDeclaration(declaration, overriddenDeclarations) is true.
      6. Let ownerDeclarations be the union of directDeclarations and projectedDeclarations.
      7. Let mergedField be the result of MergeProjectedOutputFields(ownerDeclarations).
      8. Set the field named fieldName on objectType to mergedField, replacing any previously merged field with that name.
      9. Record ownerDeclarations as the effective owners of (objectType, fieldName). For every declaration, retain its original source schema, source-local declaring type, and field definition. For each projected declaration, also retain its contributing interface and the declared or derived provenance of the implements edges through which objectType implements that interface.
CollectOverriddenDeclarations(schemas)
  1. Let implementationEdges be the result of MergeInterfaceImplementations(schemas).
  2. Let drops be an empty set.
  3. For each field declaration declaration on an object type in schemas annotated with @override:
    1. Add every declaration in CollectOverrideTargets(declaration, schemas, implementationEdges) to drops.
  4. Return drops. The source schemas and their declarations remain unchanged.
CollectOverrideTargets(declaration, schemas, implementationEdges)
  1. Let declaringType be the object type that declares declaration.
  2. Let declaringSchema be its source schema.
  3. Let from be the value of the from argument of the @override directive on declaration.
  4. Let sourceSchema be the source schema named from in schemas.
  5. If sourceSchema does not exist or is declaringSchema:
    1. Return an empty set.
  6. Let targetTypeNames be the set containing the name of declaringType.
  7. If declaringType is annotated with @interfaceObject:
    1. For each pair (typeName, the name of declaringType) in implementationEdges:
      1. Add typeName to targetTypeNames.
  8. Let targets be an empty set.
  9. For each typeName in targetTypeNames:
    1. Let localType be the type named typeName in sourceSchema, or null if no such type exists.
    2. If localType is an object type and declares a field named the name of declaration:
      1. Add that field declaration to targets.
  10. Return targets.
ContractFieldDeclarations(interface, schemas)
  1. Let interfaceName be the name of interface.
  2. Let overriddenDeclarations be the result of CollectOverriddenDeclarations(schemas).
  3. Return the set of all fields declared on interface types named interfaceName or on stand-ins for interface across schemas, excluding types marked with @internal and fields marked with @internal. Exclude fields in overriddenDeclarations, except retain those marked with @inaccessible, which must still suppress the composed field.
MergeContractFields(interface, schemas)
  1. Let declarations be the result of ContractFieldDeclarations(interface, schemas).
  2. Let fieldNames be the names of fields in declarations.
  3. Let contractFields be an empty set.
  4. For each fieldName in fieldNames:
    1. Let fields be the fields named fieldName in declarations.
    2. Let mergedField be the result of MergeOutputFields(fields).
    3. If mergedField is not null:
      1. Add mergedField to contractFields.
  5. Return contractFields.
ContributedFields(interface, schemas)
  1. Let contractFields be the result of MergeContractFields(interface, schemas).
  2. Let overriddenDeclarations be the result of CollectOverriddenDeclarations(schemas).
  3. Let contributedFields be an empty set.
  4. For each contractField in contractFields:
    1. If any stand-in for interface across schemas declares a field declaration named the name of contractField, for which IsEligibleOwnerDeclaration(declaration, overriddenDeclarations) is true and which is not selected by any @key directive on its stand-in:
      1. Add contractField to contributedFields.
  5. Return contributedFields.
ContributingDeclarations(type, fieldName, schemas, mergedSchema)
  1. Let overriddenDeclarations be the set recorded on mergedSchema by MergeSchemas(schemas).
  2. Let candidates be an empty set.
  3. Let interfaces be the interface types that type implements in mergedSchema, including type itself if it is an interface type.
  4. For each interface in interfaces:
    1. If ContributedFields(interface, schemas) contains no field named fieldName:
      1. Continue.
    2. For each stand-in standIn for interface across schemas:
      1. For each field declaration named fieldName on standIn:
        1. If IsEligibleOwnerDeclaration(declaration, overriddenDeclarations) is true and declaration is not selected by any @key directive on standIn:
          1. Add declaration, retaining interface as its contributing interface, to candidates.
  5. Return candidates.
IsEligibleOwnerDeclaration(declaration, overriddenDeclarations)
  1. If declaration is in overriddenDeclarations, or is annotated with @external, @internal, or @inaccessible:
    1. Return false.
  2. If the type declaring declaration is annotated with @internal or @inaccessible:
    1. Return false.
  3. Return true.
MergeProjectedOutputFields(declarations)
  1. If any declaration in declarations is marked with @inaccessible:
    1. Return null.
  2. Apply Output Field Types Mergeable, Field Argument Types Mergeable, and Field With Missing Required Arguments to declarations as a single field group, including when their source-local declaring type names differ. If any check fails, composition fails with that rule’s error code.
  3. Return the result of MergeOutputFields(declarations).
Explanatory Text

ProjectInterfaceObjectFields(schemas, mergedSchema) merges the fields that stand-ins contribute into the composed interface contracts and implementing object types. It also records every eligible owner declaration for each projected object field. The distributed executor may use any reachable owner that satisfies the field’s requirements. Several owners may remain only when all of their declarations are shareable and their signatures are compatible.

Stand-Ins and Preconditions

A stand-in, as defined by @interfaceObject in Section 2 — Source Schema, is an object type that shares the name of an interface defined by at least one other source schema. It contributes non-key field implementations to the interface’s implementing types. Its fields also participate in the interface contract. The stand-in itself becomes an interface in the composed schema, and references to it refer to that interface.

MergeSchemas(schemas) dispatches a type group containing an interface and its stand-ins to MergeInterfaceTypes(types, overriddenDeclarations). It completes and attaches every object’s and interface’s implements clause before invoking projection. The relation includes edges declared in source schemas and edges derived from the transitive interface hierarchy.

Applying Overrides

CollectOverriddenDeclarations(schemas) collects every override target from the original source declarations before any field signatures are merged. It retains those declarations for source-local lookup planning and diagnostics; merging and owner selection exclude the recorded targets. Collecting all targets before applying their exclusion makes the result independent of source schema and interface iteration order. An overridden declaration’s @inaccessible annotation continues to hide that field in the composed schema.

An ordinary object field’s @override(from: ...) targets declarations of the same field on the same object type in the named schema. A stand-in field’s @override also targets declarations on implementing object types and on stand-ins for more-specific interfaces in that schema. Real interface field contracts are never override targets. The existing override validation rules apply to these target declarations.

Projected fields are not source declarations, so an implementing object cannot use @override to target an inherited stand-in implementation. A surviving direct declaration and every applicable projected declaration must satisfy the sharing rule together.

Extending Interface Contracts

Stand-in fields merge with same-named real interface fields using MergeOutputFields(fields). Internal stand-in types and internal fields do not participate. An inaccessible contract field remains absent and contributes no projected implementation. External fields may describe a contract but supply no unconditional implementation. A field selected by a stand-in’s @key participates in the contract but supplies no projected implementation either. These exclusions apply to each source declaration separately.

Every non-key field contributed by a stand-in also participates in the contracts of the interface’s sub-interfaces. Composition merges all applicable declarations for each sub-interface field together, including its own contract declarations and all inherited stand-in declarations. It therefore reconciles field types and arguments across the complete set, even when several ancestors contribute the same field. No interface iteration order selects the field signature.

Arguments annotated with @require are excluded from the composed field by MergeOutputFields(fields). The distributed executor supplies those arguments. The normal field and argument merging rules also apply across different source-local parent type names, preventing incompatible projected declarations from being hidden behind a single copied field definition.

Resolving Effective Owners

Every eligible direct declaration and every applicable projected declaration remains in the effective owner set. A more-specific interface or an implementing object receives no automatic precedence. When more than one declaration remains, all declarations must be shareable, including those projected through different interfaces or declared in the same source schema. Shareability follows IsShareableDeclaration(declaration), including object-level @shareable and the existing implicit sharing of key fields. Violations involving projection are reported by INVALID_PROJECTED_FIELD_SHARING.

For each projected object field, composition always rebuilds the field signature from the complete effective owner set. It does this even if the object already has a field with that name, or if every remaining owner is projected. An overridden non-null declaration cannot leave a non-null signature behind when the surviving owner is nullable. Likewise, implementations projected from different interfaces all participate in signature merging.

Post-merge validation checks the resulting field against every interface contract that the object or sub-interface implements. A mergeable owner set is insufficient if its result violates one of those contracts. Inaccessible fields on implementing types are not reintroduced by projection; normal interface validation reports a missing required field when applicable.

Ownership Metadata

For each object type and projected field, composition retains every effective owner’s original source schema, source-local declaring type, and field signature. Two declarations in one source schema remain distinct owner options when their local parent types differ. A projected owner additionally records its contributing interface and the declared or derived implements edges that make it applicable.

The satisfiability rules and distributed executor use this information to reach each owner in that source schema’s own type vocabulary and supply its lookup inputs and requirements. Edge provenance also lets diagnostics explain why a field is projected onto an object whose source schema never declared the corresponding interface relationship.

Examples

Here source schema B contributes taxRate to Product, Chair, and Table. Only schema A defines the concrete implementing types.

Example № 213# Source Schema A
type Query {
  productById(id: ID!): Product @lookup
}

interface Product @key(fields: "id") {
  id: ID!
  name: String!
}

type Chair implements Product @key(fields: "id") {
  id: ID!
  name: String!
}

type Table implements Product @key(fields: "id") {
  id: ID!
  name: String!
}

# Source Schema B
type Query {
  productTaxById(id: ID!): Product @lookup @internal
}

type Product @interfaceObject @key(fields: "id") {
  id: ID!
  taxRate: Float
}

The effective owner of both Chair.taxRate and Table.taxRate is source schema B. Its key field id merges into the Product contract and provides lookup identity, but does not project an implementation onto Chair.id or Table.id. It is already implicitly shareable as part of @key. The stand-in itself does not appear in the composed schema; every reference to it becomes a reference to Product.

To share Chair.taxRate with a direct implementation, schema B can mark its Product.taxRate declaration with @shareable and schema C can add:

Example № 214# Source Schema C
type Query {
  chairTaxById(id: ID!): Chair @lookup @internal
}

type Chair @key(fields: "id") {
  id: ID!
  taxRate: Float @shareable
}

Both schemas B and C are then effective owners of Chair.taxRate; B remains the sole owner of Table.taxRate. The distributed executor may use either reachable implementation for Chair.taxRate. If either taxRate declaration is not shareable, composition fails with INVALID_PROJECTED_FIELD_SHARING. The same rule applies when the two declarations coexist in one source schema.

The following hierarchy also permits sharing across stand-ins. Schema A provides the complete concrete-type lookup; B and C contribute interchangeable implementations at two interface levels.

Example № 215# Source Schema A
type Query {
  productById(id: ID!): Product @lookup
}

interface Product @key(fields: "id") {
  id: ID!
}

interface PhysicalProduct implements Product @key(fields: "id") {
  id: ID!
}

type Chair implements PhysicalProduct & Product @key(fields: "id") {
  id: ID!
}

# Source Schema B
type Query {
  productWeightById(id: ID!): Product @lookup @internal
}

type Product @interfaceObject @key(fields: "id") {
  id: ID!
  weight: Int @shareable
}

# Source Schema C
type Query {
  physicalProductWeightById(id: ID!): PhysicalProduct @lookup @internal
}

type PhysicalProduct @interfaceObject @key(fields: "id") {
  id: ID!
  weight: Int @shareable
}

Both B and C own Chair.weight. C’s more-specific interface gives it no precedence. The distributed executor may use either reachable implementation. The composed PhysicalProduct.weight and Chair.weight fields both use the complete applicable set of declarations.

Finally, an override can move a directly declared field to a stand-in. Schema B below takes over A’s Book.value declaration and adds value to the Media contract.

Example № 216# Source Schema A
type Query {
  mediaById(id: ID!): Media @lookup
}

interface Media @key(fields: "id") {
  id: ID!
}

type Book implements Media @key(fields: "id") {
  id: ID!
  value: String!
}

# Source Schema B
type Query {
  mediaValueById(id: ID!): Media @lookup @internal
}

type Media @interfaceObject @key(fields: "id") {
  id: ID!
  value: String @override(from: "A")
}

# Composite Schema
interface Media {
  id: ID!
  value: String
}

type Book implements Media {
  id: ID!
  value: String
}

Only B owns Book.value. Both composed value fields are nullable, matching the surviving declaration. The overridden String! declaration does not affect the final signature.

3.3.9Merge Output Fields

Formal Specification
MergeOutputFields(fields)
  1. If any field in fields is marked with @inaccessible
    1. Return null
  2. Filter out all fields marked with @internal from fields.
  3. If fields is empty:
    1. Return null
  4. Let firstField be the first field in fields.
  5. Let fieldName be the name of firstField.
  6. Let fieldTypes be the list of types of each field in fields.
  7. Let fieldType be the result of LeastRestrictiveType(fieldTypes).
  8. Let description be the description of firstField.
  9. For each field in fields:
    1. If description is null:
      1. Let description be the description of field.
  10. Let mergedArguments be an empty set.
  11. Let argumentNames be the set of all argument names in fields.
  12. For each argumentName in argumentNames:
    1. Let arguments be the set of arguments with the name argumentName in fields
    2. If length of arguments is not equal to the length of fields:
      1. Continue.
    3. If any argument in arguments is marked with @inaccessible:
      1. Continue.
    4. If any argument in arguments is marked with @require:
      1. Continue.
    5. Let mergedArgument be the result of MergeArgumentDefinitions(arguments).
    6. If mergedArgument is not null:
      1. Add mergedArgument to mergedArguments.
  13. Return a new field with the name of fieldName, type of fieldType, arguments of mergedArguments, and description of description.
Explanatory Text

The MergeOutputFields(fields) algorithm is used when multiple fields across different object or interface types share the same field name and must be merged into a single composed field. This algorithm ensures that the final composed schema has one definitive definition for that field, resolving differences in type, description, and arguments.

Inaccessible Fields

If any of the fields is marked with @inaccessible, the entire merged field is discarded by returning null. A field that cannot be exposed in a composed schema prevents the field from being composed at all.

Internal Fields

Any field marked with @internal is removed from consideration before merging begins. This ensures that internal fields do not appear in the final composed schema and also do not affect the merging process. Internal fields are intended for internal use only and are not part of the composed schema and can collide in their definitions.

In the case where all fields are marked with @internal, the field will not appear in the composed schema.

Combining Descriptions

The first field that defines a description is used as the description for the merged field. If no description is found, the merged field will have no description.

Determining the Field Type

The return type of the composed field is determined by invoking LeastRestrictiveType(fieldTypes) with the complete list of field return types. This helper function computes a type that is compatible with all the provided field types, ensuring that the composed schema does not break schemas expecting any of those types. The calculation is order-independent. For example, LeastRestrictiveType(fieldTypes) might unify String! and String into String, or A and U into U when U is a union that contains A.

Merging Arguments

Each field can declare arguments. The algorithm collects all argument names across these fields and merges them using MergeArgumentDefinitions(arguments). Before merging, any arguments marked with @inaccessible or @require are excluded. If this exclusion causes the number of arguments available for a given name to differ from the total number of fields – or if at least one schema omits the argument – the argument is skipped entirely. Otherwise, any differences in argument type, default value, or description are resolved via the merging rules in MergeArgumentDefinitions(arguments).

Example

Imagine two schemas with a discountPercentage field on a Product type that slightly differ in return type:

Example № 217# Schema A

type Product {
  """
  Computes a discount as a percentage of the product's list price.
  """
  discountPercentage(percent: Int = 10): Int!
}

# Schema B

type Product {
  discountPercentage(percent: Int): Int
}

# Composed Result

type Product {
  """
  Computes a discount as a percentage of the product's list price.
  """
  discountPercentage(percent: Int): Int
}

If the argument is missing in one of the schemas, the composed field will not include that argument:

Example № 218# Schema A
type Product {
  discountPercentage(percent: Int): Int
}

# Schema B
type Product {
  discountPercentage: Int
}

# Composed Result
type Product {
  discountPercentage: Int
}

In case one argument is marked with @inaccessible, the composed field will not include that argument:

Example № 219# Schema A
type Product {
  discountPercentage(percent: Int): Int
}

# Schema B
type Product {
  discountPercentage(percent: Int @inaccessible): Int
}

# Composed Result
type Product {
  discountPercentage: Int
}

In case a schema defines a requirement through the @require directive, the composed field will not include that argument

Example № 220# Schema A
type Product {
  discountPercentage(percent: Int): Int
  discount: Int
}

# Schema B
type Product {
  discountPercentage(percent: Int @require(field: "discount")): Int
}

# Composed Result
type Product {
  discountPercentage: Int
}

3.3.10Merge Input Fields

Formal Specification
MergeInputFields(fields)
  1. If any field in fields is marked with @inaccessible
    1. Return null
  2. Let firstField be the first field in fields.
  3. Let fieldName be the name of firstField.
  4. Let fieldType be the type of firstField.
  5. Let description be the description of firstField.
  6. Let defaultValue be the default value of firstField or undefined if none exists.
  7. For each field in fields:
    1. Assert: field is not marked with @inaccessible
    2. Let type be the type of field.
    3. Set fieldType to be the result of MostRestrictiveType(fieldType, type).
    4. If description is null:
      1. Let description be the description of field.
    5. If defaultValue is undefined:
      1. Set defaultValue to the default value of field or undefined if none exists.
  8. Return a new input field with the name of fieldName, type of fieldType, and description of description and default value of defaultValue.
Explanatory Text

The MergeInputFields(fields) algorithm merges multiple input field definitions, all sharing the same field name, into a single composed input field. This ensures the final input type in a composed schema maintains a consistent type, description, and default value for that field. Below is a breakdown of how MergeInputFields(fields) operates:

Inaccessible Fields

Before calling MergeInputFields(fields), all fields marked with @inaccessible must be filtered out. If any such field appears in the input, it is a precondition violation of this algorithm.

Combining Descriptions

The name of the merged field is taken from the first field in the list. The description is set to the first non-null description encountered among the fields. If no description is found, the merged field will have no description.

Combining Field Types

The merged field type is computed by calling MostRestrictiveType(typeA, typeB). Unlike output fields, where LeastRestrictiveType(fieldTypes) is used, input fields often follow stricter constraints. If one source schema defines a field as non-nullable and another as nullable, the merged field type must be non-nullable to satisfy both schemas. MostRestrictiveType(typeA, typeB) ensures a final input type that is compatible with all definitions of that field.

Inheriting Default Values

If multiple fields define default values, whichever appears first in the list effectively wins. If there are non compatible default values, the pre merge validation has already asserted that the default values are compatible.

Examples

Suppose we have two input type definitions for the same OrderFilter input field, defined in separate schemas:

Example № 221# Schema A

input OrderFilter {
  """
  Filter by the minimum order total
  """
  minTotal: Int = 0
}

# Schema B

input OrderFilter {
  minTotal: Int!
}

# Composed Result

input OrderFilter {
  """
  Filter by the minimum order total
  """
  minTotal: Int! = 0
}

In the final schema, minTotal is defined using the most restrictive type (Int!), has a default value of 0, and includes the description from the original field in Schema A.

3.3.11Merge Argument Definitions

Formal Specification
MergeArgumentDefinitions(arguments)
  1. If any argument in arguments is marked with @inaccessible
    1. Return null
  2. Let mergedArgument be the first argument in arguments that is not marked with @require
  3. If mergedArgument is null
    1. Return null
  4. For each argument in arguments:
    1. Assert: argument is not marked with @inaccessible
    2. Assert: argument is not marked with @require
    3. Set mergedArgument to the result of MergeArguments(mergedArgument, argument)
  5. Return mergedArgument
Explanatory Text

MergeArgumentDefinitions(arguments) merges multiple arguments that share the same name across different field definitions into a single composed argument definition.

Inaccessible Arguments

Inaccessible arguments (@inaccessible) should be handled and filtered out before calling MergeArgumentDefinitions(arguments). By the time this algorithm is invoked, any arguments marked @inaccessible must already be removed. If such an argument somehow appears here, it is a precondition violation of this algorithm.

Handling @require

The @require directive is likewise handled before this algorithm. Arguments marked with @require do not participate in the merge process and must be filtered out of the input. If any @require arguments are included in this function, it is also a precondition violation.

Merging Arguments

All remaining arguments (those not marked @inaccessible or @require) are merged via MergeArguments(mergedArgument, argument). This algorithm ensures that the final composed argument is compatible with all definitions of that argument, resolving differences in type, default value, and description.

By selectively merging differences where possible, this algorithm ensures that @require), and merging differences where possible, this algorithm ensures that the resulting composed argument is both valid and compatible with the source definitions.

Example

Consider two field definitions that share the same filter argument, but with slightly different types and descriptions:

Example № 222# Schema A

type Query {
  searchProducts(
    """
    Filter to apply to the search
    """
    filter: ProductFilter!
  ): [Product]
}

# Schema B

type Query {
  searchProducts(
    """
    Search filter to apply
    """
    filter: ProductFilter
  ): [Product]
}

# Composed Result

type Query {
  searchProducts(
    """
    Filter to apply to the search
    """
    filter: ProductFilter!
  ): [Product]
}

In the merged schema, the filter argument is defined with the most restrictive type (ProductFilter!), includes the description from the original field in Schema A, and is marked as required.

3.3.12Merge Arguments

Formal Specification
MergeArguments(argumentA, argumentB)
  1. Let typeA be the type of argumentA.
  2. Let typeB be the type of argumentB.
  3. Let type be MostRestrictiveType(typeA, typeB).
  4. Let description be the description of argumentA or undefined if none exists.
  5. If description is undefined:
    1. Let description be the description of argumentB.
  6. Let defaultValue be the default value of argumentA or undefined if none exists.
  7. If defaultValue is undefined:
    1. Set defaultValue to the default value of argumentB or undefined if none exists.
  8. Return a new argument with the name of argumentA, type of type, description of description, and default value of defaultValue.
Explanatory Text

MergeArguments(argumentA, argumentB) takes two arguments with the same name but possibly differing in type, description, or default value, and returns a single, unified argument definition.

Unifying the Type

The algorithm uses MostRestrictiveType(typeA, typeB) to determine the final argument type. For input positions (like arguments), the most restrictive type is needed to ensure that the merged argument type accepts all values the sources demand. For instance, if one argument type is String! and the other is String, the merged type must be String! so that it remains valid from both perspectives.

Choosing the Description

The description of the first argument is used if it is defined, otherwise the description of the second argument is used.

Inheriting the Default Value

The algorithm takes the first defined default value it encounters. Pre-merge validation has already asserted that any differing defaults are compatible.

Examples

Suppose we have two field definitions that share the same limit argument, but differ in type, description, and default value:

Example № 223# Schema A

type Query {
  products(limit: Int = 10): [Product]
}

# Schema B

type Query {
  products(
    """
    Number of items to fetch
    """
    limit: Int!
  ): [Product]
}

# Composed Result

type Query {
  products(
    """
    Number of items to fetch
    """
    limit: Int! = 10
  ): [Product]
}

3.3.13Shared Algorithms

3.3.13.1Least Restrictive Type

Formal Specification
LeastRestrictiveType(types)
  1. Assert: types is not empty.
  2. Let isNullable be true.
  3. If every type in types is a non nullable type:
    1. Set isNullable to false.
  4. Let unwrappedTypes be the list produced by replacing each non nullable type in types with its inner type.
  5. If any type in unwrappedTypes is a list type:
    1. Assert: every type in unwrappedTypes is a list type.
    2. Let innerTypes be the list of inner types of each type in unwrappedTypes.
    3. Let innerType be LeastRestrictiveType(innerTypes).
    4. If isNullable is true:
      1. Return innerType as a nullable list type.
    5. Otherwise:
      1. Return innerType as a non nullable list type.
  6. Otherwise:
    1. Let namedType be LeastRestrictiveNamedOutputType(unwrappedTypes).
    2. If isNullable is true:
      1. Return namedType as a nullable type.
    3. Otherwise:
      1. Return namedType as a non nullable type.
LeastRestrictiveNamedOutputType(namedTypes)
  1. Assert: every type in namedTypes is a named output type.
  2. Let candidates be the set of unique types in namedTypes.
  3. Let supertypeCandidates be the set of all candidate in candidates for which IsOutputSupertype(candidate, type) is true for every type in namedTypes.
  4. Assert: supertypeCandidates is not empty.
  5. Sort supertypeCandidates by:
    1. the number of possible runtime object types in ascending order, with scalar and enum types having zero possible runtime object types.
    2. the candidate type name in ascending lexical order.
  6. Return the first member of supertypeCandidates.
IsOutputSupertype(candidate, type)
  1. If candidate and type are the same named type:
    1. Return true.
  2. If either candidate or type is a scalar or enum type:
    1. Return false.
  3. If candidate is an object type:
    1. Return false.
  4. If type is an object type:
    1. Return true if type is a possible runtime object type of candidate.
    2. Otherwise return false.
  5. Return true if every possible runtime object type of type is also a possible runtime object type of candidate.
  6. Otherwise return false.
Explanatory Text

LeastRestrictiveType(types) identifies a single type that safely handles all possible runtime values produced by the sources defining the types in types. The algorithm considers all types together, so the selected type is independent of source schema order. If one source can return null while another cannot, the merged type becomes nullable to avoid runtime exceptions – because a strictly non-null signature would be violated whenever null appears. Similarly, if all sources enforce non-null, the result remains non-null.

Nullability

When merging types of differing nullability (e.g., one String! vs. another String), the presence of a nullable type in one source effectively dictates that the final type must accept null. If either source can produce null, a strictly non-null field would break the contract if null were ever returned.

Lists

If both sources provide a list type, then the function unifies those list types by merging their inner types (e.g., the element type of the list). Whether the list itself is nullable depends on whether both sources treat the list as non-null. In other words, if any source can return null for the list, the final list type must also be nullable.

Named Output Types

When the unwrapped types are leaf types, the algorithm requires the same scalar or enum type. If they differ (e.g., String vs. Int), the schemas are fundamentally incompatible for merging, yet the pre merge validation should have already caught this issue.

When the unwrapped types are object, interface, or union types, the algorithm selects one of the declared return types that is a supertype of every other declared return type. A supertype candidate covers another composite type when it can represent every possible runtime object type of that type. Other than exact equality, object types are not supertype candidates for interface or union types. The most specific covering candidate is selected by choosing the candidate with the smallest possible runtime object type set, with remaining ties broken by type name. This ensures that field type selection is deterministic and does not depend on source schema order.

Examples

In the following scenario, one source might return null, so the resulting merged type must allow null.

Example № 224# Schema A
type Product {
  price: Float!
}

# Schema B
type Product {
  price: Float
}

# Merged Result
type Product {
  price: Float
}

Here, both sources use lists of Int, but they differ in nullability. Consequently, the merged list type is [Int], which permits a null list or null elements.

Example № 225# Schema A
type Product {
  ratings: [Int]!
}

# Schema B
type Product {
  ratings: [Int!]
}

# Merged Result
type Product {
  ratings: [Int]
}

Here, one source returns object type Product and the other returns union type FeaturedItem. Since FeaturedItem contains Product, FeaturedItem is the least restrictive return type regardless of source schema order.

Example № 226# Schema A
type Query {
  featured: Product
}

type Product {
  id: ID
}

# Schema B
type Query {
  featured: FeaturedItem
}

union FeaturedItem = Product

type Product {
  id: ID
}

# Merged Result
type Query {
  featured: FeaturedItem
}

union FeaturedItem = Product

type Product {
  id: ID
}

3.3.13.2Most Restrictive Type

Formal Specification
MostRestrictiveType(typeA, typeB)
  1. Let isNullable be false.
  2. If typeA and typeB are nullable types:
    1. Set isNullable to true.
  3. If typeA is a non nullable type:
    1. Set typeA to the inner type of typeA.
  4. If typeB is a non nullable type:
    1. Set typeB to the inner type of typeB.
  5. If typeA is a list type:
    1. Assert: typeB is a list type.
    2. Let innerTypeA be the inner type of typeA.
    3. Let innerTypeB be the inner type of typeB.
    4. Let innerType be MostRestrictiveType(innerTypeA, innerTypeB).
    5. If isNullable is true:
      1. Return innerType as a nullable list type.
    6. Otherwise:
      1. Return innerType as a non nullable list type.
  6. Otherwise
    1. Assert: typeA is equal to typeB
    2. If isNullable is true:
      1. Return typeA as a nullable type.
    3. Otherwise:
      1. Return typeA as a non nullable type.
Explanatory Text

MostRestrictiveType(typeA, typeB) determines a single input type that strictly honors the constraints of both sources. If either source requires a non-null value, the merged type also becomes non-null so that no invalid (e.g., null) data can be introduced at runtime. Conversely, if both sources allow null, the merged type remains nullable. The same principle applies to list types, where the more restrictive settings (non-null list or non-null elements) are used.

Nullability

For input fields, if either source are non null, it’s unsafe to allow null in the merged schema. Consequently, when one type is non-nullable (String!) and the other is nullable (String), the resulting type is non-nullable (String!). Only if both types are explicitly nullable does the merged type remain nullable (e.g., String).

Lists

When merging list types, both sources must be lists. Inside the list, the same merging logic applies: if either source disallows null elements (e.g., [Int!] vs. [Int]), the final merged list also disallows null elements to avoid unexpected runtime failures. If both lists can have null elements, then the merged list similarly allows null.

Scalar Types

Like other merging steps, if the underlying base types (e.g., String vs. Int) differ, the types cannot be reconciled. A merged schema cannot reinterpret String as Int, so the process fails if there’s a fundamental mismatch. This should already be caught by the pre merge validation.

Examples

Here, because one source disallows null, the final merged type must also disallow null to avoid a situation where a null could be passed where it isn’t allowed:

Example № 227# Schema A
input ProductFilter {
  currency: String!
}

# Schema B
input ProductFilter {
  currency: String
}

# Merged Result
input ProductFilter {
  currency: String!
}

In the following example, since one definition mandates non-null items ([Int!]), it is more restrictive and prevents null elements in the list. Additionally, the other source mandates a non-null list ([Int]!). The merged result, [Int!]!, preserves these constraints to ensure the field does not accept or produce values that violate either source.

Example № 228# Schema A
input ProductFilter {
  ratings: [Int!]
}

# Schema B
input ProductFilter {
  ratings: [Int]!
}

# Merged Result
input ProductFilter {
  ratings: [Int!]!
}

3.4Post Merge Validation

After the schema is composed, there are certain validations that are only possible in the context of the fully merged schema. These validations verify overall consistency: for example, ensuring that no type is left without accessible fields, or that interfaces and their implementors remain compatible. This stage confirms that the combined schema remains coherent when considered as a whole.

3.4.1Validate Type System

3.4.1.1Invalid Merged GraphQL

Error Code

INVALID_MERGED_GRAPHQL

Severity

ERROR

Formal Specification
Explanatory Text

Source schemas can each be valid while their combination violates a GraphQL type-system rule. The merged schema must also pass GraphQL validation.

For example, one source schema may declare PhysicalProduct implements Product and another may declare Product implements PhysicalProduct. Each declaration can be valid in its source schema, but combining them creates a cycle. Composition fails instead of emitting a type that implements itself.

3.4.1.2No Queries

Error Code

NO_QUERIES

Severity

ERROR

Formal Specification
  • Let fields be the set of all fields in the Query type of the merged schema.
  • fields must not be empty.
Explanatory Text

This rule ensures that the composed schema includes at least one accessible field on the root Query type.

In GraphQL, the Query type is essential as it defines the entry points for read operations. If none of the composed schemas expose any query fields, the composed schema would lack a root query, making it an invalid GraphQL schema.

Examples

In this example, at least one schema provides accessible query fields, satisfying the rule.

# Schema A
type Query {
  product(id: ID!): Product
}

type Product {
  id: ID!
}
# Schema B
type Query {
  review(id: ID!): Review
}

type Review {
  id: ID!
  content: String
  rating: Int
}

Even if some query fields are marked as @inaccessible, as long as there is at least one accessible query field in the composed schema, the rule is satisfied.

In this case, Schema A exposes an internal query field internalData marked with @inaccessible, making it hidden in the composed schema. However, Schema B provides an accessible product query field. Therefore, the composed schema has at least one accessible query field, adhering to the rule.

# Schema A
type Query {
  internalData: InternalData @inaccessible
}

type InternalData {
  secret: String
}
# Schema B
type Query {
  product(id: ID!): Product
}

type Product {
  id: ID!
  name: String
}

If all query fields in all schemas are marked as @inaccessible, the composed schema will lack accessible query fields, violating the rule.

In the following counter-example, both schemas have query fields, but all are marked as @inaccessible.

This means there are no accessible query fields in the composed schema, triggering the NO_QUERIES error.

# Schema A
type Query {
  internalData: InternalData @inaccessible
}

type InternalData {
  secret: String
}
# Schema B
type Query {
  adminStats: AdminStats @inaccessible
}

type AdminStats {
  userCount: Int
}

3.4.1.3Reference To Inaccessible Type

Error Code

REFERENCE_TO_INACCESSIBLE_TYPE

Formal Specification
  • Let inputFields be the set of all accessible fields of the input types in the composed schema.
  • For each inputField in inputFields:
    • Let namedType be the named type that inputField references
    • namedType must be accessible.
  • Let outputFields be the set of all accessible fields of the output types in the composed schema.
  • For each outputField in outputFields:
    • Let namedType be the named type that outputField references
    • namedType must be accessible.
  • Let arguments be the set of all accessible arguments of the output fields in the composed schema.
  • For each argument in arguments:
    • Let namedType be the named type that argument references
    • namedType must be accessible.
Explanatory Text

In a composed schema, fields and arguments must only reference types that are exposed. This requirement guarantees that public types do not reference inaccessible structures which are intended for internal use.

A valid case where a public input field references another public input type:

Example № 229input Input1 {
  field1: String!
  field2: Input2
}

input Input2 {
  field3: String
}

Another valid case is where the field is not exposed in the composed schema:

Example № 230input Input1 {
  field1: String!
  field2: Input2 @inaccessible
}

input Input2 @inaccessible {
  field3: String
}

An invalid case is when an input field references an inaccessible type:

Counter Example № 231input Input1 {
  field1: String!
  field2: Input2!
}

input Input2 @inaccessible {
  field3: String
}

3.4.1.4Reference To Internal Type

Error Code

REFERENCE_TO_INTERNAL_TYPE

Formal Specification
  • Let fields be the set of all fields of the output types in the composed schema.
  • For each field in fields:
    • Let namedType be the named type that field references
    • namedType must exist in the composed schema.
Explanatory Text

In a composed schema, fields must not reference internal types. This requirement guarantees that public types do not reference internal structures which are intended for internal use.

A valid case where a public field references another public type:

Example № 232type Object1 {
  field1: String!
  field2: Object2
}

type Object2 {
  field3: String
}

Another valid case is where the field is internal in the source schema:

Example № 233type Object1 {
  field1: String!
  field2: Object2 @internal
}

type Object2 @internal {
  field3: String
}

An invalid case is when a field references an internal type:

Counter Example № 234type Object1 {
  field1: String!
  field2: Object2!
}

type Object2 @internal {
  field3: String
}

3.4.2Validate Composite Types

3.4.2.1Empty Merged Object Type

Error Code

EMPTY_MERGED_OBJECT_TYPE

Severity

ERROR

Formal Specification
  • Let types be the set of all object types in the composed schema.
  • For each type in types:
    • Let fields be a set of all fields in type.
    • fields must not be empty.
Explanatory Text

For object types defined across multiple source schemas, the merged object type is the superset of all fields defined in these source schemas. However, any field marked with @inaccessible in any source schema is hidden and not included in the merged object type. An object type with no fields, after considering @inaccessible annotations, is considered empty and invalid.

Examples

In the following example, the merged object type Author is valid. It includes all fields from both source schemas, with age being hidden due to the @inaccessible directive in one of the source schemas:

# Schema A

type Author {
  name: String
  age: Int @inaccessible
}

# Schema B
type Author {
  age: Int
  registered: Boolean
}

If the @inaccessible directive is applied to an object type itself, the entire merged object type is excluded from the composite execution schema, and it is not required to contain any fields.

# Schema A

type Author @inaccessible {
  name: String
  age: Int
}

# Schema B
type Author {
  registered: Boolean
}

This counter-example demonstrates an invalid merged object type. In this case, Author is defined in two source schemas, but all fields are marked as @inaccessible in at least one of the source schemas, resulting in an empty merged object type:

Counter Example № 235# Schema A

type Author {
  name: String @inaccessible
  registered: Boolean
}

# Schema B

type Author {
  name: String
  registered: Boolean @inaccessible
}

3.4.2.2Empty Merged Interface Type

Error Code

EMPTY_MERGED_INTERFACE_TYPE

Severity

ERROR

Formal Specification
  • Let types be the set of all interface types in the composed schema.
  • For each type in types:
    • Let fields be a set of all fields in type.
    • fields must not be empty.
Explanatory Text

For interface types defined across multiple source schemas, the merged interface type is the superset of all fields defined in these source schemas. However, any field marked with @inaccessible in any source schema is hidden and not included in the merged interface type. An interface type with no fields, after considering @inaccessible annotations, is considered empty and invalid.

Examples

In the following example, the merged object type Product is valid. It includes all fields from both source schemas, with price being hidden due to the @inaccessible directive in one of the source schemas:

# Schema A
interface Product {
  name: String
  price: Int @inaccessible
}

# Schema B
interface Product {
  name: String
  inStock: Boolean
}

If the @inaccessible directive is applied to an interface type itself, the entire merged interface type is excluded from the composite execution schema, and it is not required to contain any fields.

# Schema A
interface Product @inaccessible {
  name: String
  price: Int
}

# Schema B
interface Product {
  name: String
  inStock: Boolean
}

This counter-example demonstrates an invalid merged interface type. In this case, Product is defined in two source schemas, but all fields are marked as @inaccessible in at least one of the source schemas, resulting in an empty merged interface type:

Counter Example № 236# Schema A
interface Product {
  name: String
  price: Int @inaccessible
}

# Schema B
interface Product {
  name: String @inaccessible
  price: Int
}

3.4.2.3Implemented by Inaccessible

Error Code

IMPLEMENTED_BY_INACCESSIBLE

Severity

ERROR

Formal Specification
  • Let schema be the merged composite execution schema.
  • Let types be the set of all object and interface types in schema.
  • For each type in types:
    • Let implementedInterfaces be the set of all interfaces implemented by type.
    • For each implementedInterface in implementedInterfaces:
      • Let interfaceFields be the set of all fields defined on implementedInterface that are visible in the merged schema.
      • For each interfaceField in interfaceFields:
        • Let fieldName be the name of interfaceField.
        • type must have a field with the name fieldName
Explanatory Text

This rule ensures that inaccessible fields (@inaccessible) on an object or interface type are not exposed through an interface. A composite type that implements an interface must provide public access to each field defined by the interface. If a field on an object type is marked as @inaccessible but implements an interface field that is visible in the composed schema, this creates a contradiction: the interface contract requires that field to be accessible, yet the implementation hides it.

This rule prevents inconsistencies in the composed schema, ensuring that every interface field visible in the composed schema is also publicly visible on all types implementing that interface.

Examples

In the following example, User.id is accessible and implements Node.id which is also accessible, no error occurs.

# The interface field `id` is visible and provided by `User` without @inaccessible.
interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  name: String
}

Since Auditable and its field lastAudit are @inaccessible, the Order.lastAudit field is allowed to be @inaccessible because it does not implement any visible interface field in the composed schema.

# The entire interface is @inaccessible, thus its fields are not publicly visible.
interface Auditable @inaccessible {
  lastAudit: DateTime!
}

type Order implements Auditable {
  lastAudit: DateTime! @inaccessible
  orderNumber: String
}

In this example, Node.id is visible in the public schema (no @inaccessible), but User.id is marked @inaccessible. This violates the interface contract because User claims to implement Node, yet does not expose the id field to the public schema.

Counter Example № 237interface Node {
  id: ID!
}

type User implements Node {
  id: ID! @inaccessible
  name: String
}

3.4.2.4Interface Field No Implementation

Error Code

INTERFACE_FIELD_NO_IMPLEMENTATION

Severity

ERROR

Formal Specification
  • Let schema be the merged composite execution schema.
  • Let implementingTypes be the object and interface types in schema.
  • For each implementingType in implementingTypes:
    • Let interfaces be the interfaces implemented by implementingType in schema.
    • For each interface in interfaces:
      • Let interfaceFields be the set of fields defined on interface that are visible in the merged schema.
      • For each field in interfaceFields:
        • If no field with the name of field is present on implementingType:
          • Produce an INTERFACE_FIELD_NO_IMPLEMENTATION error.
Explanatory Text

In GraphQL, any object or interface type that implements an interface must provide a field definition for every field declared by that interface. If an object type fails to implement a particular field required by one of its interfaces, the composite schema becomes invalid because the resulting schema breaks the contract defined by that interface.

This rule checks that object and interface types merged from different sources correctly implement all interface fields. In scenarios where a schema defines an interface field, but the implementing object type in another schema omits that field, an error is raised.

Examples

In this valid example, the User interface has three fields: id, name, and email. Both the RegisteredUser and GuestUser types implement all three fields, satisfying the interface contract.

Example № 238# Schema A
interface User {
  id: ID!
  name: String!
  email: String
}

type RegisteredUser implements User {
  id: ID!
  name: String!
  email: String
  lastLogin: DateTime
}

# Schema B
interface User {
  id: ID!
  name: String!
  email: String
}

type GuestUser implements User {
  id: ID!
  name: String!
  email: String
  temporaryCartId: String
}

In this counter-example, the User interface is defined with three fields, but the GuestUser type omits one of them (email), causing an INTERFACE_FIELD_NO_IMPLEMENTATION error.

Although GuestUser implements User, it does not provide the email field. Since the merged schema sees that the interface User has email but GuestUser does not provide it, the schema composition fails with the INTERFACE_FIELD_NO_IMPLEMENTATION error.

Counter Example № 239# Schema A
interface User {
  id: ID!
  name: String!
  email: String
}

type RegisteredUser implements User {
  id: ID!
  name: String!
  email: String
  lastLogin: DateTime
}

# Schema B
interface User {
  id: ID!
  name: String!
}

type GuestUser implements User {
  id: ID!
  name: String!
  temporaryCartId: String
}

3.4.2.5Invalid Projected Field Sharing

Error Code

INVALID_PROJECTED_FIELD_SHARING

Severity

ERROR

Formal Specification
  • Let schemas be the source schemas.
  • Let schema be the merged composite execution schema.
  • For each object type type in schema:
    • For each field field on type:
      • Let owners be the effective owner declarations recorded for (type, field) by ProjectInterfaceObjectFields, or an empty set if none were recorded.
      • If no owner is a projected declaration, or owners has fewer than two declarations:
        • Continue to the next field.
      • For each owner in owners:
  • For each interface type interface in schema:
    • For each field field on interface:
      • Let fieldName be the name of field.
      • Let contributors be ContributingDeclarations(interface, fieldName, schemas, schema).
      • If contributors has fewer than two declarations:
        • Continue to the next field.
      • For each contributor in contributors:
IsShareableDeclaration(declaration)
  1. If declaration is annotated with @shareable:
    1. Return true.
  2. If the object type declaring declaration is annotated with @shareable:
    1. Return true.
  3. If declaration is implicitly shareable under the rules of @key:
    1. Return true.
  4. Return false.
Explanatory Text

A field may have both a direct implementation on an object type and an implementation projected from a stand-in. It may also have implementations projected from several stand-ins, including stand-ins for interfaces related by implementation. Every eligible declaration remains an owner. Neither a direct declaration nor a more-specific interface takes precedence.

When more than one declaration remains, every declaration must be shareable. This includes declarations in the same source schema when they have different local parent types. The executor may use any reachable owner. Sharing requires semantically interchangeable implementations and compatible field signatures; marking a field @shareable does not make incompatible signatures valid.

The owner set is the same one used to construct the merged field and validate satisfiability. It excludes external fields, internal fields and types, stand-in key-only references, and overridden declarations. Fields removed from the merged interface contract do not produce projected owners. This keeps validation and projection consistent when @inaccessible removes a contributed field.

If sharing is not intended, a source schema must remove the colliding implementation. Composition does not choose an implementation based on interface specificity. For interfaces, only implementation contributors from the interface’s own stand-ins and its ancestors’ stand-ins are checked; ordinary contract declarations are excluded. Ordinary fields with only direct owners continue to use Invalid Field Sharing.

Implementations should aggregate the diagnostic by the contributing declarations and identify each source schema and local field coordinate. The diagnostic should offer the two resolutions: make every eligible implementation shareable, or remove the colliding implementation.

Examples

In this counter-example, Book.reviews has both a direct implementation and an implementation projected from Media. Neither declaration is shareable, so composition fails with INVALID_PROJECTED_FIELD_SHARING.

Counter Example № 240# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
}

type Book implements Media @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review @shareable {
  rating: Int!
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review @shareable {
  rating: Int!
}

Marking both declarations shareable resolves the collision. Both source schemas remain eligible to resolve Book.reviews, subject to satisfiability.

Example № 241# Source Schema A
type Book implements Media @key(fields: "id") {
  id: ID!
  reviews: [Review!]! @shareable
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]! @shareable
}

The same rule applies when a type implements unrelated interfaces whose stand-ins contribute the same field, or when one contributing interface implements another. Every eligible declaration must be shareable; an interface hierarchy does not resolve the collision.

An external field supplies no projected implementation. In the following example, source schema B declares title only for the @provides optimization on featured. It does not become another owner of Book.title, so that field needs no sharing annotation.

Example № 242# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
}

type Query {
  mediaById(id: ID!): Media @lookup
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  title: String! @external
}

type Query {
  featured: Media @provides(fields: "title")
}

A field removed from the interface contract also supplies no projected implementation. Here Media.rating is inaccessible, while Book.rating remains a direct field. Source schema B’s declaration does not create a sharing conflict on Book.rating.

Example № 243# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  rating: Int @inaccessible
}

type Book implements Media @key(fields: "id") {
  id: ID!
  rating: Int
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  rating: Int
}

3.4.2.6Interface Field Type Mismatch

Error Code

INTERFACE_FIELD_TYPE_MISMATCH

Severity

ERROR

Formal Specification
  • Let schema be the merged composite execution schema.
  • Let types be the object and interface types in schema.
  • For each type in types:
    • For each interface implemented by type in schema:
      • For each field interfaceField on interface:
        • Let field be the field with the same name on type.
        • If field does not exist:
          • Continue to the next interfaceField.
        • Let fieldType be the type of field.
        • Let interfaceFieldType be the type of interfaceField.
        • IsImplementationFieldType(fieldType, interfaceFieldType, schema) must be true.
IsImplementationFieldType(type, interfaceType, schema)
  1. If interfaceType is non-null:
    1. If type is not non-null:
      1. Return false.
    2. Let innerType and innerInterfaceType be the inner types of type and interfaceType, respectively.
    3. Return IsImplementationFieldType(innerType, innerInterfaceType, schema).
  2. If type is non-null:
    1. Let innerType be the inner type of type.
    2. Return IsImplementationFieldType(innerType, interfaceType, schema).
  3. If interfaceType is a list type:
    1. If type is not a list type:
      1. Return false.
    2. Let innerType and innerInterfaceType be the inner types of type and interfaceType, respectively.
    3. Return IsImplementationFieldType(innerType, innerInterfaceType, schema).
  4. If type is a list type:
    1. Return false.
  5. If type and interfaceType have the same name:
    1. Return true.
  6. If interfaceType is an interface and type is an object or interface type that implements interfaceType in schema:
    1. Return true.
  7. If interfaceType is a union and type is an object member of that union:
    1. Return true.
  8. Return false.
Explanatory Text

Merging all eligible owners determines a field’s return type. The resulting field must still satisfy every interface contract implemented by its parent object or interface. This is the GraphQL requirement that an implementing field return the same type or a permitted subtype of the interface field’s type, including its list and non-null wrappers.

This check runs after projection and the completion of the implements relation. It catches incompatible combinations even when the contributing declarations have different local parent names and every source schema is valid in isolation. Sharing does not relax interface contracts.

For example, suppose Chair implements both PhysicalProduct and DigitalProduct. Their stand-ins contribute label: String! @shareable and label: String @shareable, respectively. Both are eligible owners, so merging produces Chair.label: String. That field cannot implement PhysicalProduct.label: String!, and composition fails with INTERFACE_FIELD_TYPE_MISMATCH. Choosing the non-null signature would also be incorrect, because the nullable owner may return null. The source schemas must agree on signatures that satisfy both interface contracts.

3.4.2.7Interface Field Argument No Implementation

Error Code

INTERFACE_FIELD_ARGUMENT_NO_IMPLEMENTATION

Severity

ERROR

Formal Specification
  • Let schema be the merged composite execution schema.
  • Let implementingTypes be the object and interface types in schema.
  • For each implementingType in implementingTypes:
    • Let interfaces be the interfaces implemented by implementingType in schema.
    • For each interface in interfaces:
      • Let interfaceFields be the set of fields defined on interface that are visible in the merged schema.
      • For each interfaceField in interfaceFields:
        • If a field with the same name as interfaceField is not present on implementingType:
          • Continue
        • Let implementingField be the field on implementingType with the same name as interfaceField.
        • Let interfaceArguments be the set of arguments on interfaceField.
        • For each interfaceArgument in interfaceArguments:
          • Let argumentName be the name of interfaceArgument.
          • An argument with the name argumentName must be present on implementingField.
Explanatory Text

In GraphQL, an object or interface field that implements an interface field must declare every argument that the interface field declares. In a composite schema, this contract can break even though every source schema is valid on its own: the merge process removes arguments that are annotated with @require or @inaccessible in a source schema, and an argument only survives merging if every source schema that contributes the field declares it. If an argument is removed from an implementing object field but survives on the merged interface field, the composite schema would break the interface contract. This rule detects such cases and fails the composition rather than producing an invalid composite schema.

Examples

In this valid example, the interface field Account.displayName and the implementing field User.displayName both declare the locale argument in the composite schema.

Example № 244# Schema A
interface Account {
  id: ID!
  displayName(locale: String): String
}

type User implements Account {
  id: ID!
  displayName(locale: String): String
}

In this counter-example, the locale argument on User.displayName is annotated with @require in Schema A but not on the interface field in Schema B, so it is removed from the implementing field but survives on the merged interface field. The merged User type no longer correctly implements Account, raising an INTERFACE_FIELD_ARGUMENT_NO_IMPLEMENTATION error.

Counter Example № 245# Schema A
type User @key(fields: "id") {
  id: ID!
  displayName(locale: String @require(field: "preferredLocale")): String
}

# Schema B
interface Account {
  id: ID!
  displayName(locale: String): String
}

type User implements Account @key(fields: "id") {
  id: ID!
  displayName(locale: String): String
  preferredLocale: String
}

3.4.2.8Interface Field Argument Type Mismatch

Error Code

INTERFACE_FIELD_ARGUMENT_TYPE_MISMATCH

Severity

ERROR

Formal Specification
  • Let schema be the merged composite execution schema.
  • For each object or interface type type in schema:
    • For each interface implemented by type in schema:
      • For each field interfaceField on interface:
        • Let field be the field with the same name on type.
        • If field does not exist:
          • Continue to the next interfaceField.
        • For each argument on field:
          • Let interfaceArgument be the argument with the same name on interfaceField.
          • If interfaceArgument exists:
            • The types of argument and interfaceArgument must be identical, including list and non-null wrappers.
          • Otherwise:
            • argument must be nullable or have a default value.
Explanatory Text

An implementing field must accept the arguments specified by its interface with exactly the same input types. Additional arguments must be optional. These GraphQL requirements apply to fields after merging and projection, including fields inherited by sub-interfaces. The preceding argument-presence rule detects missing arguments; this rule detects incompatible surviving arguments or an additional required argument.

For example, a composed interface field accepting locale: String cannot be implemented by a field accepting locale: String!. A field may add format: String, or format: String! = "short", but cannot add format: String! without a default. Sharing a projected implementation does not relax these requirements.

3.4.3Validate Input Types

3.4.3.1Empty Merged Input Object Type

Error Code

EMPTY_MERGED_INPUT_OBJECT_TYPE

Severity

ERROR

Formal Specification
  • Let inputTypes be the set of all input object types in the composite schema.
  • For each inputType in inputTypes:
    • Let fields be a set of all fields in inputType.
    • fields must not be empty.
Explanatory Text

For input object types defined across multiple source schemas, the merged input object type is the intersection of all fields defined in these source schemas. Any field marked with the @inaccessible directive in any source schema is hidden and not included in the merged input object type. An input object type with no fields, after considering @inaccessible annotations, is considered empty and invalid.

Examples

In the following example, the merged input object type BookFilter is valid.

input BookFilter {
  name: String
}

input BookFilter {
  name: String
}

If the @inaccessible directive is applied to an input object type itself, the entire merged input object type is excluded from the composite execution schema, and it is not required to contain any fields.

input BookFilter @inaccessible {
  name: String
  minPageCount: Int
}

input BookFilter {
  name: String
}

This counter-example demonstrates an invalid merged input object type. In this case, BookFilter is defined in two source schemas, but all fields are marked as @inaccessible in at least one of the source schemas, resulting in an empty merged input object type:

Counter Example № 246input BookFilter {
  name: String @inaccessible
  paperback: Boolean
}

input BookFilter {
  name: String
  paperback: Boolean @inaccessible
}

Here is another counter-example where the merged input object type is empty because no fields intersect between the two source schemas:

Counter Example № 247input BookFilter {
  paperback: Boolean
}

input BookFilter {
  name: String
}

3.4.3.2Non-Null Input Fields cannot be inaccessible

Error Code

NON_NULL_INPUT_FIELD_IS_INACCESSIBLE

Formal Specification
  • Let fields be the set of all fields across all input types in all source schemas.
  • For each field in fields:
    • If field is a non-null input field:
      • Let coordinate be the coordinate of field.
      • coordinate must be in the composed schema.
Explanatory Text

When an input field is declared as non-null in any source schema, it imposes a hard requirement: queries or mutations that reference this field must provide a value for it. If the field is then marked as @inaccessible or removed during schema composition, the final schema would still implicitly demand a value for a field that no longer exists in the composed schema, making it impossible to fulfill the requirement.

As a result:

  • Nullable (optional) fields can be hidden or removed without invalidating the composed schema, because the user is never required to supply a value for them.
  • Non-null (required) fields, however, must remain exposed in the composed schema so that users can provide values for those fields. Hiding a required input field breaks the schema contract and leads to an invalid composition.
Examples

The following is valid because the age field, although @inaccessible in one source schema, is nullable and can be safely omitted in the final schema without breaking any mandatory input requirement.

Example № 248# Schema A
input BookFilter {
  author: String!
  age: Int @inaccessible
}

# Schema B
input BookFilter {
  author: String!
  age: Int
}

# Composite Schema
input BookFilter {
  author: String!
}

Another valid case is when a nullable input field is removed during merging:

Example № 249# Schema A
input BookFilter {
  author: String!
  age: Int
}

# Schema B
input BookFilter {
  author: String!
}

# Composite Schema
input BookFilter {
  author: String!
}

An invalid case is when a non-null input field is inaccessible:

Counter Example № 250# Schema A
input BookFilter {
  author: String!
  age: Int!
}

# Schema B
input BookFilter {
  author: String!
  age: Int @inaccessible
}

# Composite Schema
input BookFilter {
  author: String!
}

Another invalid case is when a non-null input field is removed during merging:

Counter Example № 251# Schema A
input BookFilter {
  author: String!
  age: Int!
}

# Schema B
input BookFilter {
  author: String!
}

# Composite Schema
input BookFilter {
  author: String!
}

3.4.4Validate Enums

3.4.4.1Empty Merged Enum Type

Error Code

EMPTY_MERGED_ENUM_TYPE

Severity

ERROR

Formal Specification
  • Let enumTypes be the set of all enum types in the composite schema.
  • For each enumType in enumTypes:
    • Let values be a set of all values in enumType.
    • values must not be empty.
Explanatory Text

Enum values have to be an exact match across all source schemas. If an enum value only exists in one source schema, it has to be marked as @inaccessible. Enum members that are marked as @inaccessible are not included in the merged enum type. An enum type with no values is considered empty and invalid.

Examples

In the following example, the merged enum type DeliveryStatus is valid. It includes all values from both source schemas, with PENDING being hidden due to the @inaccessible directive in one of the source schemas:

# Schema A
enum DeliveryStatus {
  PENDING @inaccessible
  SHIPPED
  DELIVERED
}

# Schema B
enum DeliveryStatus {
  SHIPPED
  DELIVERED
}

If the @inaccessible directive is applied to an enum type itself, the entire merged enum type is excluded from the composite execution schema, and it is not required to contain any values.

# Schema A
enum DeliveryStatus @inaccessible {
  SHIPPED
  DELIVERED
}

# Schema B
enum DeliveryStatus {
  SHIPPED
  DELIVERED
}

This counter-example demonstrates an invalid merged enum type. In this case, DeliveryStatus is defined in two source schemas, but all values are marked as @inaccessible in at least one of the source schemas, resulting in an empty merged enum type:

Counter Example № 252# Schema A
enum DeliveryStatus {
  PENDING @inaccessible
  DELIVERED
}

# Schema B
enum DeliveryStatus {
  PENDING
  DELIVERED @inaccessible
}

3.4.4.2Enum Type Default Value Inaccessible

Error Code

ENUM_TYPE_DEFAULT_VALUE_INACCESSIBLE

Formal Specification
ValidateArgumentDefaultValues()
  1. Let arguments be the set of all arguments of fields in the composed schema
  2. For each argument in arguments
    1. If argument has a default value:
      1. Let defaultValue be the default value of argument
      2. If not ValidateDefaultValue(defaultValue)
        1. return false
  3. return true
ValidateInputFieldDefaultValues()
  1. Let inputFields be the set of all input fields in the composed schema
  2. For each inputField in inputFields:
    1. If inputField has a default value:
      1. Let defaultValue be the default value of inputField
      2. If ValidateDefaultValue(defaultValue) is false
        1. return false
  3. return true
ValidateDefaultValue(defaultValue)
  1. If defaultValue is a ListValue:
    1. For each valueNode in defaultValue:
      1. If ValidateDefaultValue(valueNode) is false
        1. return false
  2. If defaultValue is an ObjectValue:
    1. Let objectFields be a list of all fields of defaultValue
    2. For each objectField in objectFields:
      1. Let value be the value of objectField
      2. If ValidateDefaultValue(value) is false
        1. return false
  3. If defaultValue is an EnumValue:
    1. If enum be the enum type of defaultValue
    2. If enum does not have a value with the name of defaultValue
      1. return false
  4. return true
Explanatory Text

This rule ensures that inaccessible enum values are not exposed in the composed schema through default values. Output field arguments and input fields must only use enum values as their default value when not annotated with the @inaccessible directive.

In this example the FOO value in the Enum1 enum is not marked with @inaccessible, hence it does not violate the rule.

# Schema A
type Query {
  field(type: Enum1 = FOO): [Baz!]!
}

enum Enum1 {
  FOO
  BAR
}

The following example violates this rule because the default value for the argument (arg) and the input field (field) references an enum value (FOO) that is marked as @inaccessible.

Counter Example № 253# Schema A
type Query {
  field(arg: Enum1 = FOO): [Baz!]!
}

input Input1 {
  field: Enum1 = FOO
}

enum Enum1 {
  FOO @inaccessible
  BAR
}

The following example violates this rule because the default value for the argument (arg) and the input field (field2) references an @inaccessible enum value (FOO) within an object value.

Counter Example № 254# Schema A
type Query {
  field(arg: Input1 = { field1: FOO }): [Baz!]!
}

input Input1 {
  field1: Enum1
  field2: Input2 = { field3: FOO }
}

input Input2 {
  field3: Enum1
}

enum Enum1 {
  FOO @inaccessible
  BAR
}

The following example violates this rule because the default value for the argument (arg) and the input field (field) references an @inaccessible enum value (FOO) within a list.

Counter Example № 255# Schema A
type Query {
  field(arg: [Enum1] = [FOO]): [Baz!]!
}

input Input1 {
  field: [Enum1] = [FOO]
}

enum Enum1 {
  FOO @inaccessible
  BAR
}

3.4.5Validate Union Types

3.4.5.1Empty Merged Union Type

Error Code

EMPTY_MERGED_UNION_TYPE

Severity

ERROR

Formal Specification
  • Let unionTypes be the set of all union types in the composite schema.
  • For each unionType in unionTypes:
    • Let members be a set of all member types in unionType.
    • members must not be empty.
Explanatory Text

For union types defined across multiple source schemas, the merged union type is the union of all member types defined in these source schemas. However, any member type marked with @inaccessible in any source schema is hidden and not included in the merged union type. A union type with no members, after considering @inaccessible annotations, is considered empty and invalid.

Examples

In the following example, the merged union type SearchResult is valid. It includes all member types from both source schemas, with User being hidden due to the @inaccessible directive in one of the source schemas:

# Schema A
union SearchResult = User | Product

type User @inaccessible {
  id: ID!
}

type Product {
  id: ID!
}

# Schema B
union SearchResult = Product | Order

type Product {
  id: ID!
}

type Order {
  id: ID!
}

# Composite Schema
union SearchResult = Product | Order

If the @inaccessible directive is applied to a union type itself, the entire merged union type is excluded from the composite execution schema, and it is not required to contain any members.

# Schema A
union SearchResult @inaccessible = User | Product

type User {
  id: ID!
}

type Product {
  id: ID!
}

# Schema B
union SearchResult = Product | Order

type Product {
  id: ID!
}

type Order {
  id: ID!
}

This counter-example demonstrates an invalid merged union type. In this case, SearchResult is defined in two source schemas, but all member types are marked as @inaccessible in at least one of the source schemas, resulting in an empty merged union type:

Counter Example № 256# Schema A
union SearchResult = User | Product

type User @inaccessible {
  id: ID!
}

type Product {
  id: ID!
}

# Schema B
union SearchResult = User | Product

type User {
  id: ID!
}

type Product @inaccessible {
  id: ID!
}

3.4.6Validate Is Directives

3.4.6.1Is Invalid Fields

Error Code

IS_INVALID_FIELDS

Severity

ERROR

Formal Specification
  • Let schemas be all source schemas.
  • Let compositeTypes be the set of all composite types in schemas.
  • For each composite in compositeTypes:
    • Let fields be the set of fields on composite.
    • Let arguments be the set of all arguments on fields.
    • For each argument in arguments:
      • If argument is not annotated with @is:
        • Continue
      • Let schema be the schema that defines argument.
      • Let declaringField be the field that defines argument.
      • Let declaringType be the type that defines declaringField.
      • Let otherSchemas be the set of all schemas excluding schema.
      • Let fieldArg be the string value of the field argument of the @is directive on argument.
      • Let parsedFieldArg be the parsed selection map from fieldArg.
      • The parsed selection map parsedFieldArg must satisfy the validation rules defined in Appendix A, Section 6.3, using:
        • declaringType as the initial root type.
        • The combined schema context formed by the union of otherSchemas as the schema context except all fields marked as @internal
        • Validation succeeds if each required field selection path can be resolved across this combined schema context. Individual fields in the selection may exist in different schemas; it is not required that all fields referenced by parsedFieldArg reside within a single schema.
Explanatory Text

Even if the field selection map for @is(field: "…") is syntactically valid, its contents must also be valid within the composed schema. Fields must exist on the parent type for them to be referenced by @is. In addition, fields referencing unknown fields break the valid usage of @is, leading to an IS_INVALID_FIELDS error.

Examples

In the following example, the @is directive’s field argument is a valid field selection map and satisfies the rule.

Example № 257# Schema A
type Query {
  personById(id: ID! @is(field: "id")): Person @lookup
}

type Person {
  id: ID!
  name: String
}

In this counter-example, the @is directive references a field (unknownField) that does not exist on the return type (Person), causing an IS_INVALID_FIELDS error.

Counter Example № 258# Schema A
type Query {
  personById(id: ID! @is(field: "unknownField")): Person @lookup
}

type Person {
  id: ID!
  name: String
}
Note An @is selection map must not supply arguments (see Is Fields Has Arguments).

3.4.7Validate Require Directives

3.4.7.1Require Invalid Fields

Error Code

REQUIRE_INVALID_FIELDS

Severity

ERROR

Formal Specification
  • Let schemas be all source schemas.
  • Let compositeTypes be the set of all composite types in schemas.
  • For each composite in compositeTypes:
    • Let fields be the set of fields on composite.
    • Let arguments be the set of all arguments on fields.
    • For each argument in arguments:
      • If argument is not annotated with @require:
        • Continue
      • Let schema be the schema that defines argument.
      • Let declaringField be the field that defines argument.
      • Let declaringType be the type that defines declaringField.
      • Let otherSchemas be the set of all schemas excluding schema.
      • Let fieldArg be the string value of the field argument of the @require directive on argument.
      • Let parsedFieldArg be the parsed selection map from fieldArg.
      • The parsed selection map parsedFieldArg must satisfy the validation rules defined in Appendix A, Section 6.3, using:
        • declaringType as the initial root type.
        • The combined schema context formed by the union of otherSchemas as the schema context except all fields marked as @internal
        • Validation succeeds if each required field selection path can be resolved across this combined schema context. Individual fields in the selection may exist in different schemas; it is not required that all fields referenced by parsedFieldArg reside within a single schema.
      • Let rootFields be the root field selections in parsedFieldArg.
      • For each rootField in rootFields:
        • declaringType in schema must not already declare rootField itself: a declared field that is not @external or @internal and is not overridden. Require only what you must fetch from another schema; the remainder of the map may return to this schema.
Explanatory Text

Even if the selection map for @require(field: "…") is syntactically valid, its contents must also be valid. Required fields must exist on the parent type in a different schema than the one defining the requirement for them to be referenced by @require. A requirement for a value the declaring schema already declares locally is rejected: if you already have it, do not require it. Additionally, requiring unknown fields invalidates @require, resulting in a REQUIRE_INVALID_FIELDS error.

@require on a field declared by an @interfaceObject stand-in is ordinary @require. The stand-in is an object type like any other, so no special rule is needed. Its selection map is validated exactly as above, resolved against the combined schema context of the other source schemas. Required arguments are excluded from the composite schema, per the existing behavior of @require. More than one source schema may contribute the same field name to the same interface. This can happen through the interface’s own declaration, or through more than one of its @interfaceObject stand-ins. In every such case, the argument definitions across those schemas must still be mergeable; see Field With Missing Required Arguments for an example of this interaction.

Examples

In the following example, the @require directive’s field argument is a valid selection set and satisfies the rule.

Example № 259# Schema A
type User @key(fields: "id") {
  id: ID!
  profile(name: String @require(field: "name")): Profile
}

type Profile {
  id: ID!
  name: String
}

# Schema B
type User @key(fields: "id") {
  id: ID!
  name: String
}

In this counter-example, the @require directive references a field (unknownField) that does not exist on the parent type (Book), causing a REQUIRE_INVALID_FIELDS error.

Counter Example № 260type Book {
  id: ID!
  pages(pageSize: Int @require(field: "unknownField")): Int
}

In the following counter-example, the @require directive references a field from itself (Book.size) which is not allowed. This results in a REQUIRE_INVALID_FIELDS error.

Counter Example № 261type Book {
  id: ID!
  size: Int
  pages(pageSize: Int @require(field: "size")): Int
}

The @require directive may also reference fields with arguments. In the following example, the weight argument is required from the weight output field selected with a constant unit argument:

Example № 262# Schema A
type Product @key(fields: "id") {
  id: ID!
  shippingCost(
    weight: Float @require(field: "weight(unit: IMPERIAL)")
  ): Currency
}

# Schema B
type Product @key(fields: "id") {
  id: ID!
  weight(unit: WeightUnit!): Float
}

Argument values within @require must be constant literals; variables are not permitted. Argument names must exist on the referenced field, values must coerce to the argument’s type, and required arguments without defaults must be supplied. When the referenced field is defined in multiple source schemas, the argument definitions across those schemas must be mergeable as defined by Field Argument Types Mergeable.

3.5Validate Satisfiability

The final step confirms that the composite schema supports executable queries without leading to invalid conditions. Each query path defined in the merged schema is checked to ensure that every field can be resolved. If any query path is unresolvable, the schema is deemed unsatisfiable, and composition fails.

3.5.1Unsatisfiable Query Path

Error Code

UNSATISFIABLE_QUERY_PATH

Severity

ERROR

Formal Specification

An execution context is a tuple (sourceSchema, localType, valueType). localType is the schema-local type on which execution can select fields. valueType is either a canonical concrete object identity in the execution type graph, whose identity is known, or an interface identity in that graph, whose identity is still opaque. Entering a stand-in by lookup preserves a known concrete identity; producing a new value through a stand-in creates an opaque identity. All helpers below use schema for the merged composite execution schema and sourceSchemas for the full source-schema set.

The execution type graph retains a canonical identity for each non-internal object, interface, and union type name in the original source schemas, including types removed from the client schema through @inaccessible. A stand-in uses the identity of its real interface. The graph retains the original local type and field declarations and the combined implements relation, completed by transitive closure before client visibility filtering. Types present in the merged schema use the same canonical identities. This graph is execution metadata; retaining an identity does not expose a type or project its fields into the client schema. Possible-type and implements comparisons in the helpers below use this graph unless explicitly scoped to a source schema.

  • Let schema be the merged composite execution schema.
  • Let sourceSchemas be the set of source schemas used to compose schema.
  • Let operationRootTypes be the operation root types defined in schema.
  • Let pending and visited be empty sets of (type, contexts) pairs.
  • For each rootType in operationRootTypes:
    • Let contexts be the set of (sourceSchema, localRootType, rootType) tuples for source schemas defining the corresponding localRootType.
    • Add (rootType, contexts) to pending.
  • While pending is not empty:
    • Remove a pair (type, contexts) from pending.
    • If (type, contexts) is in visited:
      • Continue to the next pair.
    • Add (type, contexts) to visited.
    • For each client-accessible field field on type:
      • Let owners be ResolveField(contexts, type, field, sourceSchemas, {}).
      • Let usableOwners and returnContexts be empty sets.
      • For each owner in owners:
        • Let values be FieldValueContexts(owner, field, schema).
        • Let ownerValues be an empty set.
        • Let usable be true.
        • For each value in values:
          • If the valueType of value is an interface:
            • Let recovered be RecoverTypeContexts(value, schema, sourceSchemas, {}).
            • If recovered is empty:
              • Set usable to false.
            • Add every context in recovered to ownerValues.
          • Otherwise:
            • Add value to ownerValues.
        • If usable is true:
          • Add owner to usableOwners.
          • Add every context in ownerValues to returnContexts.
      • usableOwners must not be empty.
      • For each distinct client-accessible valueType in returnContexts:
        • Let nextContexts be the contexts in returnContexts whose valueType is valueType.
        • Add (valueType, nextContexts) to pending.

The worklist represents executable query-path continuations. Record a witness path for each pair for diagnostics. Equal pairs need only be checked once; recursive fields that change the available contexts produce a different pair and must be checked. The source schemas and types are finite, so the set of possible pairs is finite. Scalar and enum fields produce no return contexts. Client selection of __typename is satisfied by a known concrete identity; opaque values must recover that identity even when no ordinary field is selected.

FieldOwnerOptions(type, field, candidateSchemas):

Returns the source schemas and schema-local parent types that may resolve field on type. Effective owner declarations retain their original source schema and local parent type.

  • Let options be an empty set of (sourceSchema, localType) tuples.
  • If composition recorded an effective owner set for (type, field):
    • For each ownerDeclaration in that owner set:
      • Let sourceSchema be the source schema defining ownerDeclaration.
      • If sourceSchema is not in candidateSchemas:
        • Continue to the next ownerDeclaration.
      • Let localType be the type declaring ownerDeclaration.
      • Add (sourceSchema, localType) to options.
    • Return options.
  • For each sourceSchema in candidateSchemas:
    • For each localType in sourceSchema that has the name of type:
      • If localType is annotated with @internal:
        • Continue to the next localType.
      • If localType declares field, that declaration is not annotated with @external or @internal, and composition did not record it as overridden:
        • Add (sourceSchema, localType) to options.
  • Return options.

ResolveField(contexts, type, field, candidateSchemas, activeGoals):

Resolve a field without discarding the context in which its parent value is available. Selecting a field directly requires both the source schema and the schema-local parent type to match its owner. Coexisting concrete types and stand-ins in one schema are distinct local parents.

  • Let results be an empty set.
  • Let ownerOptions be FieldOwnerOptions(type, field, candidateSchemas).
  • For each context in contexts:
    • For each (targetSchema, targetType) in ownerOptions:
      • Let entries be EnterType(context, targetSchema, targetType, candidateSchemas, activeGoals).
      • For each entry in entries:
        • If the local declaration of field on entry is not an eligible owner according to FieldOwnerOptions for its valueType:
          • Continue to the next entry.
        • If ResolveRequirements(context, entry, field, candidateSchemas, activeGoals) is true:
          • Add entry to results.
  • Return results.

EnterType(context, targetSchema, targetType, candidateSchemas, activeGoals):

A lookup transitions the current value to an owner’s local parent type. Its input paths are evaluated from context, not from targetType. The target must belong to candidateSchemas, but routing inputs may use the full sourceSchemas. Excluding a schema as an owner of a required field does not prevent reading its already available key fields to reach an allowed owner.

  • If targetSchema is not in candidateSchemas:
    • Return an empty set.
  • If the sourceSchema and localType of context are targetSchema and targetType, respectively:
    • Return the set containing context.
  • Let goal be the tuple (context, targetSchema, targetType, candidateSchemas).
  • If goal is in activeGoals:
    • Return an empty set.
  • Let nextGoals be activeGoals with goal added.
  • Let results be an empty set.
  • For each field lookup in targetSchema annotated with @lookup:
    • Let entries be LookupResultContexts(context, lookup, targetType).
    • If entries is empty:
      • Continue to the next lookup.
    • If LookupInputsResolvable(lookup, context, nextGoals) is true:
      • Add every context in entries to results.
  • Return results.

LookupResultContexts(context, lookup, targetType):

Determine whether lookup can enter targetType for the current value and which identities are available afterward. All possible-type comparisons use type names; source-local and composite type definitions are distinct objects.

  • Let targetSchema be the source schema defining lookup.
  • Let returnType be the unwrapped return type of lookup in targetSchema.
  • Let valueType be the valueType of context.
  • If returnType is a stand-in:
    • If returnType is not targetType:
      • Return an empty set.
    • Let interface be the interface identity in the execution type graph with the name of returnType.
    • If valueType is neither interface nor a type that implements interface in the execution type graph:
      • Return an empty set.
    • Return the set containing (targetSchema, targetType, valueType).
  • If valueType is an interface:
    • If returnType is not an interface with the name of valueType, or targetType does not have that name:
      • Return an empty set.
    • If GetPossibleTypes(returnType) in targetSchema does not contain every type in GetPossibleTypes(valueType) in the execution type graph:
      • Return an empty set.
    • Return the set of (targetSchema, localObjectType, objectType) tuples for every possible objectType of valueType in the execution type graph, where localObjectType has the name of objectType in targetSchema.
  • If targetType does not have the name of valueType:
    • Return an empty set.
  • If returnType has the name of valueType, or is an interface or union whose source-local possible types contain valueType:
    • Return the set containing (targetSchema, targetType, valueType).
  • Return an empty set.

ResolveRequirements(context, owner, field, candidateSchemas, activeGoals):

Each @require argument on the owner’s local field must be supplied from schemas other than the schema declaring that requirement. This exclusion applies to that dependency’s owners. A nested requirement establishes its own owner exclusion; earlier exclusions do not accumulate across independent fields.

  • Let targetSchema and targetType be the sourceSchema and localType of owner.
  • Let requiredArguments be the arguments annotated with @require on field of targetType in targetSchema.
  • If requiredArguments is empty:
    • Return true.
  • Let allowedSchemas be sourceSchemas excluding targetSchema.
  • Let goal be the tuple (context, owner, field, allowedSchemas).
  • If goal is in activeGoals:
    • Return false.
  • Let nextGoals be activeGoals with goal added.
  • For each requiredArgument in requiredArguments:
    • Let fieldSelectionMap be the field argument value of @require on requiredArgument.
    • If SelectionMapResolvable(fieldSelectionMap, targetType, requiredArgument, context, allowedSchemas, nextGoals) is false:
      • Return false.
  • Return true.

LookupInputsResolvable(lookup, context, activeGoals):

Every lookup argument must be constructible from the current value. Its map is validated against the lookup’s local return type, while execution starts from context. Routing inputs may use every source schema.

  • Let rootType be the unwrapped return type of lookup.
  • For each argument on lookup:
    • If argument has an @is directive:
      • Let fieldSelectionMap be the field argument value of @is.
    • Otherwise:
      • Let fieldSelectionMap be the name of argument.
    • If SelectionMapResolvable(fieldSelectionMap, rootType, argument, context, sourceSchemas, activeGoals) is false:
      • Return false.
  • Return true.

SelectionMapResolvable(fieldSelectionMap, rootType, argument, context, candidateSchemas, activeGoals):

Evaluate the complete map, retaining its type-conditioned alternatives. A lookup or requirement map is declared against its destination type, but its first selection uses the source context’s local parent and known identity. For example, Media.id in a stand-in lookup’s map selects Book.id from a native Book, and selects the local Media.id from an opaque stand-in.

  • Let cases be the guarded cases represented by fieldSelectionMap for argument, rooted at rootType, according to Appendix A. Retain:
    • The field paths, including literal arguments, that construct each input value and the path prefixes at which type conditions are evaluated.
    • The type conditions governing each alternative and its required paths.
    • The input-object and list structure, expected input types, and any outcomes permitted by the existing mapping and input-coercion rules.
  • Let state contain cases, with the empty output-path prefix available in the set containing context, and no other paths yet selected.
  • Return ResolveSelectionCases(state, candidateSchemas, activeGoals).
ResolveSelectionCases(state, candidateSchemas, activeGoals)
  1. See Resolve Selection Cases.

ResolveInputField(contexts, type, field, candidateSchemas, activeGoals):

Dependency inputs may use a key already available on the current stand-in even when another declaration owns the same composite field. This is a local input capability, not an additional projected owner or a reason to enter that stand-in from another context.

  • Let results be ResolveField(contexts, type, field, candidateSchemas, activeGoals).
  • For each context in contexts:
    • Let sourceSchema and localType be its source schema and local parent.
    • If sourceSchema is not in candidateSchemas, or localType is not a stand-in, or localType is annotated with @internal:
      • Continue to the next context.
    • If field is selected by a @key on localType, its local declaration is not annotated with @external or @internal, and it was not overridden:
      • If ResolveRequirements(context, context, field, candidateSchemas, activeGoals) is true:
        • Add context to results.
  • Return results.

FieldValueContexts(owner, field, schema):

A field produces new values. Its local return type determines their initial execution context; this is independent of the local parent on which the field was resolved.

  • Let sourceSchema and localType be the sourceSchema and localType of owner.
  • Let returnType be the unwrapped return type of field on localType in sourceSchema.
  • If returnType is a scalar or enum:
    • Return an empty set.
  • If returnType is a stand-in:
    • Let interface be the interface identity in the execution type graph with its name.
    • Return the set containing (sourceSchema, returnType, interface).
  • Let localPossibleTypes be the set containing returnType if it is an object type, or GetPossibleTypes(returnType) in sourceSchema otherwise.
  • Return the set of (sourceSchema, localObjectType, objectType) tuples for each non-internal localObjectType in localPossibleTypes, where objectType is its canonical object identity in the execution type graph. Do not filter these identities by membership or visibility in schema.

RecoverTypeContexts(context, schema, candidateSchemas, activeGoals):

Recover an opaque value through a covering interface lookup: one lookup returning the real interface whose source-local possible types cover every composite possible type. Successful recovery preserves access to the original stand-in as well as establishing a native context for each concrete identity.

  • Let interface be the valueType of context.
  • Assert: interface is an interface identity in the execution type graph.
  • Let results be an empty set.
  • For each targetSchema in candidateSchemas:
    • Let targetType be the type with the name of interface in targetSchema.
    • If targetType is not an interface:
      • Continue to the next targetSchema.
    • Let entries be EnterType(context, targetSchema, targetType, candidateSchemas, activeGoals).
    • For each entry in entries:
      • Add entry to results.
      • Add (sourceSchema, localType, valueType) to results, where sourceSchema and localType are those of context, and valueType is the concrete valueType of entry.
  • Return results.

Recursive lookup and requirement probes use branch-local activeGoals. A repeated pending goal supplies no execution capability and fails that branch; other lookup and guarded-map alternatives must still be tried. Goals contain only source schemas, types, fields, and schema subsets, so probes terminate. This rejects circular prerequisites while permitting multi-step lookup routes that have independently resolvable inputs.

Explanatory Text

The satisfiability phase must ensure that every executable field path in the composed API can be fulfilled by at least one valid query plan. The worklist checks each distinct combination of a composite parent type and available execution contexts. Each context retains its source schema, local parent type, and the concrete identity or opaque interface of the current value. Checking a repeated combination once is sufficient; reaching the same field with different contexts requires another check. A shareable producer whose opaque result cannot recover its type is discarded when another producer can supply that field. A field fails only when no usable owner remains.

For an ordinary field, owner options are the source schemas that declare the field on the path’s current type. For a field projected from an @interfaceObject, owner options come from the effective owner set recorded during merge. A projected owner declares the field on its stand-in type, while a direct owner declares it on the implementing object type. A shareable field therefore contributes one planning option for every effective owner, and the planner retains whichever options are reachable in the current context.

The algorithm continues directly when the source schema and local parent type both match the owner. Otherwise, a compatible @lookup must establish the owner’s local parent context. Its inputs are selected from the original context. Moving between a concrete type and a coexisting stand-in may therefore require a lookup even within one source schema.

Likewise, if a field declares @require dependencies, those dependencies must also be resolvable from schemas other than the one defining that requirement. FieldSelectionMap alternatives retain their type conditions. The planner selects an owner before considering its possible returned identities, and each identity must have a complete applicable mapping. Different identities may use different alternatives of the same map. If a required runtime case has no executable mapping, that owner cannot supply the input.

If every candidate is eliminated for any field path, the path is unsatisfiable and composition fails with UNSATISFIABLE_QUERY_PATH.

A source schema defines a field marked with @external but does not resolve it; external fields are therefore never resolution candidates in the source schema that declares them. Likewise, a recorded effective owner is a candidate only while its source schema remains in the allowed schema set. Recording ownership does not bypass the exclusion imposed by a @require dependency. Internal fields and internal parent types do not supply ordinary fields or dependency inputs. Internal lookup fields remain usable for transitions. Inaccessible fields may supply executor inputs, but the client worklist traverses only types and fields present in the client-facing schema.

The @provides directive is an execution-time optimization that allows a source schema to return external fields as part of the same response when resolving the annotated field. Each @provides selection must itself be deliverable by the providing source schema, which is enforced by the @provides validation rules. Query-path satisfiability, however, is evaluated as if all @provides directives were ignored: a @provides may reduce the number of fetches in a query plan, but must never be required to make a query path satisfiable.

Opaque Values

A source schema may declare an object type annotated with @interfaceObject as the stand-in for an interface. Values produced through that schema’s fields and lookups are then opaque. Within the stand-in schema, the value is only an instance of the local object type. That schema carries no authoritative concrete type for the value in the composite schema. A __typename resolved there returns the local object type, which is wrong in the composite schema. Values obtained through a schema that defines the real interface are not opaque. The demands described here arise only where a stand-in schema produces the value. The stand-in declares its key fields as its own fields. Lookup planning must check that the particular inputs of a covering lookup are resolvable from that local context; a key declaration alone does not establish a route.

Demanding Type Context

A query path demands type context at an opaque position when it selects __typename, or when it applies a type condition that narrows the interface to one of its possible types. Any client-executable selection on an interface-typed value may select __typename. Every opaque position reached through a client-executable path therefore demands type context. A declaration on an unreachable type does not introduce a demand. Entering a stand-in through an executor lookup preserves an identity already known from a native context; that transition does not produce a new opaque value. A field subsequently selected on the stand-in that returns another stand-in does produce a new opaque value and requires recovery when reachable by clients.

Demanding Non-Local Data

A query path demands non-local data at an opaque position when it selects fields beyond those the stand-in schema itself declares: interface fields contributed by other source schemas, or fields declared by an implementing type. Such selections must be resolved by other source schemas. Resolving them for a specific value first requires recovering that value’s identity. A selection may instead require no type context and select only fields the stand-in declares. In that case, a single request to the stand-in schema is a correct and complete plan, and no recovery is needed.

Covering Interface Lookups

Both demands are met by the same capability. A covering interface lookup for an interface at an opaque position is a lookup, in a single source schema, that returns the interface itself, whose required inputs are resolvable from the stand-in’s key fields, and whose schema-local possible-type set for the interface is a superset of the composite schema’s possible-type set at that position.

The superset condition is essential because a schema’s lookup can only ever return types that the schema defines. A lookup may receive a key that identifies a concrete type its schema does not define. In that case it cannot produce the value. It either misreports or silently drops data. The comparison must be made against the composite schema’s possible-type set, not against the view of any single source schema. This is because the merged implements relation is the union of all per-schema implements edges, completed by transitive closure. Any source schema may add an implementing type, and a derived implements edge may widen the composite possible-type set beyond every individual schema’s local view.

Coverage must come from a single schema’s lookup. Two lookups in different schemas may have possible-type sets that are jointly, but not individually, a superset. Such lookups do not combine. Choosing which of them to call for a given opaque value would itself require the type identity that is being recovered. A covering interface lookup may be annotated with @internal. It is a capability of the executor, not of clients. The interface declares a compatible key as required by the interface-object key validation rules. No particular source schema is required to supply the covering lookup. The requirement arises only where a reachable opaque position demands recovery.

Reaching Projected Fields

Every non-key field of a stand-in supplies an implementation that composition may project onto each implementing type. The effective owner set is resolved during merge (see Resolving Effective Owners in Project Interface Object Fields). When the stand-in is the only owner, its data lives only in the stand-in schema, so one of that schema’s lookups must be reachable, with the value’s key fields, from every context that can resolve values of the implementing type.

When @shareable preserves both a projected declaration and one or more direct declarations, the planner may choose any reachable effective owner. The stand-in need not be reachable from a particular context when another effective owner is reachable from that context. Conversely, merely marking declarations as @shareable does not make an otherwise unreachable owner usable.

Reachability is checked from execution contexts that occur on client paths, including paths through other projected fields. Merely declaring an implementing type in a source schema does not make that schema a producing context. Likewise, a stand-in lookup entered with a known concrete identity does not independently demand a covering interface lookup.

A stand-in may contribute fields without a lookup when every path needing those fields can resolve them locally or through another shareable owner. It may also serve only as a reference by declaring key fields. Composition fails when a reachable path needs a projected field and no eligible owner can be entered.

Diagnostics

Failures raised by these clauses are reported as UNSATISFIABLE_QUERY_PATH errors. The error message must name the failing query path and the missing capability. For a missing covering interface lookup, the message must name the interface, the opaque position, and the possible types that no single schema’s lookup covers. The composite possible-type set may have been widened by an implements edge added during transitive-closure completion. In that case, the message must also state the derived edge and name the source schema whose declarations introduced it. For a projected field that no plan can reach, the message must name the contributing schema, the interface, and the field. For example: “Source schema B contributes fields to Media but provides no lookup to resolve them.”

Examples

The following query path:

Query.me.profile.age

is represented as:

[(Query, me), (User, profile), (Profile, age)]

Similarly, this path:

Mutation.createUser.query.me

is represented as:

[(Mutation, createUser), (CreateUserPayload, query), (Query, me)]

In the following example, source schema A defines the interface Media with its implementing types, and source schema B contributes a reviews field to Media through a stand-in:

Example № 263# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

type Book implements Media {
  id: ID!
  title: String!
  author: String!
}

type Movie implements Media {
  id: ID!
  title: String!
  director: String!
}

type Query {
  mediaById(id: ID!): Media @lookup
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  body: String!
}

type Query {
  mediaById(id: ID!): Media @lookup @internal
  topReviewed(limit: Int = 10): [Media!]!
}

Values produced by topReviewed and by source schema B’s mediaById are opaque. The query path [(Query, topReviewed), (Book, author)] demands both type context and non-local data. It demands type context because the path narrows Media to Book. It demands non-local data because author is not declared by the stand-in. Source schema A’s mediaById is a covering interface lookup. It returns Media itself. Its id argument is resolvable from the stand-in’s key field id. Its schema-local possible-type set {Book, Movie} is a superset of the composite possible-type set {Book, Movie}. The projected field reviews can be reached. Source schema B’s mediaById is reachable with id from every context that resolves Book or Movie. The composition is satisfiable.

The two lookup directions retain different local parent types. Starting from topReviewed, source schema B supplies the covering lookup’s input by selecting Media.id on its stand-in. It need not define Book.id. After recovery, author can be selected on source schema A’s Book, and reviews remains available on the original stand-in value. Conversely, starting with a native Book in source schema A, the lookup into B takes its input from A’s Book.id; B’s local parent for the projected owner is Media. The transition preserves the known identity Book and does not need another covering lookup.

The same transition works when an additional source schema defines only this partial view of Book, without defining Media:

Example № 264# Source Schema C
type Book @key(fields: "id") {
  id: ID!
}

type Query {
  bookById(id: ID!): Book @lookup
}

For Query.bookById.reviews, source schema C supplies Book.id directly, and source schema B resolves Media.reviews. Requiring C to define Media.id would incorrectly reject this plan. If the only way to obtain a lookup input is to invoke that same lookup first, the repeated active goal fails. An alternative lookup whose inputs are independently available may still establish a valid route.

In the following counter-example, the reviews schema contributes the non-key field reviews to Media but provides no lookup:

Counter Example № 265# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

type Book implements Media {
  id: ID!
  title: String!
}

type Movie implements Media {
  id: ID!
  title: String!
}

type Query {
  mediaById(id: ID!): Media @lookup
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  # The stand-in contributes "reviews", but source schema B declares no
  # lookup through which the executor could ever fetch it.
  reviews: [Review!]!
}

type Review {
  body: String!
}

The field reviews is projected onto Book and Movie, so the query path [(Query, mediaById), (Book, reviews)] is executable in the composite schema. Only source schema B holds the data for reviews. Source schema B declares no lookup for its stand-in, so no plan can reach it from source schema A. Composition fails with an error such as “Source schema B contributes fields to Media but provides no lookup to resolve them.” Had the stand-in declared only its key field id, it would have contributed no projected fields. It would have remained a valid, reference-only stand-in.

In the following counter-example, a third schema adds an implementing type that source schema A does not define, breaking coverage:

Counter Example № 266# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

type Book implements Media {
  id: ID!
  title: String!
  author: String!
}

type Movie implements Media {
  id: ID!
  title: String!
  director: String!
}

type Query {
  mediaById(id: ID!): Media @lookup
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Review {
  body: String!
}

type Query {
  mediaById(id: ID!): Media @lookup @internal
  topReviewed(limit: Int = 10): [Media!]!
}

# Source Schema C
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

# "Photo" is not defined by source schema A, so source schema A's "mediaById"
# can no longer answer for every possible type of "Media".
type Photo implements Media {
  id: ID!
  title: String!
  width: Int!
}

type Query {
  photoById(id: ID!): Photo @lookup
}

The composite possible-type set of Media is now {Book, Movie, Photo}. Source schema A’s mediaById has the schema-local possible-type set {Book, Movie}. Source schema C defines Media with the schema-local possible-type set {Photo}. No single source schema’s lookup covers the composite set. Every query path that demands type context or non-local data at an opaque position is unsatisfiable. This includes, for example, Query.topReviewed with __typename selected, or [(Query, topReviewed), (Book, author)]. The error must name the failing path, the interface Media, and the uncovered possible type Photo, together with source schema C, which introduced it. For example: “The query path Query.topReviewed cannot be satisfied: values of Media produced by source schema B are opaque, and no source schema provides a lookup for Media that covers the possible type Photo introduced by source schema C.” Composition succeeds again once some source schema both defines every possible type of Media and provides an interface lookup, for example when source schema A also declares type Photo implements Media with at least its key fields.

A recorded projected owner must also respect dependency exclusions. A chain of requirements or lookups that eventually returns to the same unresolved goal cannot supply the missing value.

Counter Example № 267# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  reviewCount: Int
}

type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
  reviewCount: Int
}

type Query {
  version: String
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  rating(filter: Int @require(field: "reviewCount")): Float
}

reviewCount exists on Media in A, so REQUIRE_INVALID_FIELDS passes. Resolving rating excludes B, and A offers no lookup to reach its reviewCount, so the requirement has no route: composition fails with UNSATISFIABLE_QUERY_PATH.

Example № 268# Source Schema A
interface Media @key(fields: "id") {
  id: ID!
  reviewCount: Int
}

type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
  reviewCount: Int
}

type Query {
  mediaById(id: ID!): Media @lookup
}

# Source Schema B
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  rating(filter: Int @require(field: "reviewCount")): Float
}

A reachable owner of reviewCount in the permitted schema A can now satisfy the requirement: composition succeeds.

Excluding a required-field owner does not exclude its routing keys. Suppose source schema A returns Book and resolves price with an argument requiring weight. Source schema B owns Book.weight and provides a lookup by id. The executor may select the already available Book.id in A to enter B and fetch weight, even though A is excluded as an owner of the required weight. The lookup does not let A satisfy the requirement with its own weight declaration.

Dependency paths also cover every runtime possibility. If a lookup input uses related.id and the selected related owner can return either Book or Movie, a route for Book.id alone is insufficient. That owner is usable only if the suffix can also resolve Movie.id. This check applies to inaccessible executor inputs even when clients cannot select the dependency.

For nested requirements, A.f may depend on B.g, which in turn depends on an independently resolvable A.h. The immediate required-field owners are B for g and A for h; A’s exclusion while resolving g does not exclude it from resolving h. A cycle A.f → B.g → A.f still fails when the repeated pending requirement has no independent resolution route.

For a map related<Book>.isbn | related<Movie>.upc, the planner selects the related owner once, then checks both possible runtime cases. A Book requires isbn; a Movie requires upc. Each case chooses its own matching alternative. If Movie.upc has no reachable owner, its applicable branch fails; a nullable argument does not turn that unavailable field into a null value. Outcomes actually permitted by the map and its input-coercion rules remain permitted.

Executor-only object identities are retained as well. If Product.details returns an inaccessible ShippingDetails, a requirement on details.weight can select the local ShippingDetails.weight even though that type is absent from the merged client schema. Neither the type nor its fields become client selections through this execution metadata.

Finally, a context already holding a stand-in’s id key may use that local key to enter another schema even if Book.id has a recorded effective owner set that omits the stand-in. ResolveInputField supplies this local capability without adding the stand-in to the owner set, requiring an otherwise unnecessary lookup, or bypassing the required-field owner exclusion.

3.5.2Resolve Selection Cases

Formal Specification

ResolveSelectionCases(state, candidateSchemas, activeGoals):

A state records the remaining guarded cases, the values already selected, and execution contexts at their output-path prefixes. Contexts at one prefix have one runtime identity; alternative access locations for that identity may be kept together. An unobserved type condition remains pending, rather than being assumed to match or not match.

  • Evaluate every type condition whose runtime identity is known in state, and discard only alternatives excluded by those conditions.
  • If the complete map result can be constructed from the selected values under an applicable case, with the shape and coercion required by Appendix A:
    • Return true.
  • Let actions be the following field-selection and type-recovery actions that advance a still-applicable or pending case in state:
    • To select a needed field field at an available output-path prefix:
      • Let contexts be the contexts at that prefix and type their common valueType.
      • Obtain owner alternatives from ResolveInputField(contexts, type, field, candidateSchemas, activeGoals).
      • Each owner whose valueType is type and whose local field accepts the selected literal arguments is a separate action.
    • To evaluate a pending type condition on an opaque prefix, or establish concrete contexts needed to resolve a field at that prefix:
      • Obtain recovered contexts from RecoverTypeContexts(context, schema, sourceSchemas, activeGoals) for the contexts available at that prefix.
      • A successful recovery is an action. Its outcomes retain both the original stand-in and the recovered native access locations for each identity.
  • For each action in actions:
    • Let outcomes be the execution possibilities produced by action:
      • A field-selection action records that field’s value as available. For a composite-valued field, use FieldValueContexts(owner, field, schema) to establish its child prefix, with one outcome per distinct valueType.
      • A type-recovery action has one outcome per possible concrete identity, grouping the recovered access locations for that identity together.
      • List selections check every possible element identity; list length does not create additional type cases.
      • Outcomes must agree with identities already observed at the same output-path prefix. Fetching another shareable representation of an available value does not give that value a different identity.
    • For each outcome, let nextState be state updated with that outcome. Keep the entire map’s alternatives available for specialization in nextState; do not choose an alternative before its type condition is known.
    • If outcomes is not empty and ResolveSelectionCases(nextState, candidateSchemas, activeGoals) is true for every outcome:
      • Return true.
  • Return false.
Explanatory Text

Each action must add a previously unavailable selection, access context, or known identity at one of the map’s finite output-path prefixes. A scalar or enum selection has one outcome recording that its value is available. The map’s existing null and input-coercion behavior is preserved; an unavailable field owner is a planning failure, not a null value or a failed type condition. Owner selection is an existential choice made before its possible returned types are checked universally. Thus related<Book>.isbn | related<Movie>.upc may use a different matching alternative for each runtime type, while still requiring an executable upc selection whenever related produces a Movie.

4Executor

A distributed GraphQL executor acts as an orchestrator that uses schema metadata to rewrite a GraphQL request into a query plan. This plan resolves the required data from subgraphs and coerces this data into the result of the GraphQL request.

4.1Configuration

The supergraph is a GraphQL IDL document that contains metadata for the query planner that describes the relationship between type system members and the type system members on subgraphs.

5Shared Types

In this section we outline directives and types that are shared between the subgraph configuration and the gateway configuration document.

5.1Name

scalar Name

The scalar Name represents a valid GraphQL type name.

5.2FieldSelection

scalar FieldSelection

The scalar FieldSelection represents a GraphQL field selection syntax.

Example № 269abc { ghi }

6Appendix A: Specification of FieldSelectionMap Scalar

6.1Introduction

This appendix focuses on the specification of the FieldSelectionMap scalar type. FieldSelectionMap is designed to express semantic equivalence between arguments of a field and fields within the result type. Specifically, it allows defining complex relationships between input arguments and fields in the output object by encapsulating these relationships within a parsable string format. It is used in the @is and @require directives.

To illustrate, consider a simple example from a GraphQL schema:

type Query {
  userById(userId: ID! @is(field: "id")): User! @lookup
}

In this schema, the userById query uses the @is directive with FieldSelectionMap to declare that the userId argument is semantically equivalent to the User.id field.

An example query might look like this:

query {
  userById(userId: "123") {
    id
  }
}

Here, it is expected that the userId “123” corresponds directly to User.id, resulting in the following response if correctly implemented:

{
  "data": {
    "userById": {
      "id": "123"
    }
  }
}

The FieldSelectionMap scalar is represented as a string that, when parsed, produces a SelectedValue.

A SelectedValue must exactly match the shape of the argument value to be considered valid. For non-scalar arguments, you must specify each field of the input type in SelectedObjectValue.

Example № 270type Query {
  findUserByName(user: UserInput! @is(field: "{ firstName: firstName }")): User
    @lookup
}
Counter Example № 271type Query {
  findUserByName(user: UserInput! @is(field: "firstName")): User @lookup
}

6.1.1Scope

The FieldSelectionMap scalar type is used to establish semantic equivalence between an argument and fields within a specific output type. This output type is always a composite type, but the way it’s determined can vary depending on the directive and context in which the FieldSelectionMap is used.

For example, when used with the @is directive, the FieldSelectionMap maps between the argument and fields in the return type of the field. However, when used with the @require directive, it maps between the argument and fields in the object type on which the field is defined.

Consider this example:

type Product {
  id: ID!
  delivery(
    zip: String!
    size: Int! @require(field: "dimension.size")
    weight: Int! @require(field: "dimension.weight")
  ): DeliveryEstimates
}

In this case, "dimension.size" and "dimension.weight" refer to fields of the Product type, not the DeliveryEstimates return type.

Consequently, a FieldSelectionMap must be interpreted in the context of a specific argument, its associated directive, and the relevant output type as determined by that directive’s behavior.

Examples

Scalar fields can be mapped directly to arguments.

This example maps the Product.weight field to the weight argument:

Example № 272type Product {
  shippingCost(weight: Float @require(field: "weight")): Currency
}

This example maps the Product.shippingWeight field to the weight argument:

Example № 273type Product {
  shippingCost(weight: Float @require(field: "shippingWeight")): Currency
}

Nested fields can be mapped to arguments by specifying the path. This example maps the nested field Product.packaging.weight to the weight argument:

Example № 274type Product {
  shippingCost(weight: Float @require(field: "packaging.weight")): Currency
}

Complex objects can be mapped to arguments by specifying each field.

This example maps the Product.width and Product.height fields to the dimension argument:

Example № 275type Product {
  shippingCost(
    dimension: DimensionInput
      @require(field: "{ width: width, height: height }")
  ): Currency
}

The shorthand equivalent is:

Example № 276type Product {
  shippingCost(
    dimension: DimensionInput @require(field: "{ width, height }")
  ): Currency
}

In case the input field names do not match the output field names, explicit mapping is required.

Example № 277type Product {
  shippingCost(
    dimension: DimensionInput @require(field: "{ w: width, h: height }")
  ): Currency
}

Even if Product.dimension has all the fields needed for the input object, an explicit mapping is always required.

This example is NOT allowed because it lacks explicit mapping:

Counter Example № 278type Product {
  shippingCost(dimension: DimensionInput @require(field: "dimension")): Currency
}

Instead, you can traverse into output fields by specifying the path.

This example shows how to map nested fields explicitly:

Example № 279type Product {
  shippingCost(
    dimension: DimensionInput
      @require(field: "{ width: dimension.width, height: dimension.height }")
  ): Currency
}

The path does NOT affect the structure of the input object. It is only used to traverse the output object:

Example № 280type Product {
  shippingCost(
    dimension: DimensionInput
      @require(field: "{ width: size.width, height: size.height }")
  ): Currency
}

To avoid repeating yourself, you can prefix the selection with a path that ends in a dot to traverse INTO the output type.

This affects how fields get interpreted but does NOT affect the structure of the input object:

Example № 281type Product {
  shippingCost(
    dimension: DimensionInput @require(field: "dimension.{ width, height }")
  ): Currency
}

This example is equivalent to the previous one:

Example № 282type Product {
  shippingCost(
    dimension: DimensionInput @require(field: "size.{ width, height }")
  ): Currency
}

The path syntax is required for lists because list-valued path expressions would be ambiguous otherwise.

This example is NOT allowed because it lacks the dot syntax for lists:

Counter Example № 283type Product {
  shippingCost(
    dimensions: [DimensionInput]
      @require(field: "{ width: dimensions.width, height: dimensions.height }")
  ): Currency
}

Instead, use the path syntax and brackets to specify the list elements:

Example № 284type Product {
  shippingCost(
    dimensions: [DimensionInput]
      @require(field: "dimensions[{ width, height }]")
  ): Currency
}

With the path syntax it is possible to also select fields from a list of nested objects:

Example № 285type Product {
    shippingCost(partIds: @require(field: "parts[id]")): Currency
}

For more complex input objects, all these constructs can be nested. This allows for detailed and precise mappings.

This example nests the weight field and the dimension object with its width and height fields:

Example № 286type Product {
  shippingCost(
    package: PackageInput
      @require(field: "{ weight, dimension: dimension.{ width, height } }")
  ): Currency
}

This example nests the weight field and the size object with its width and height fields:

Example № 287type Product {
  shippingCost(
    package: PackageInput
      @require(field: "{ weight, size: dimension.{ width, height } }")
  ): Currency
}

The label can be used to nest values that aren’t nested in the output.

This example nests Product.width and Product.height under dimension:

Example № 288type Product {
  shippingCost(
    package: PackageInput
      @require(field: "{ weight, dimension: { width, height } }")
  ): Currency
}

In the following example, dimensions are nested under dimension in the output:

Example № 289type Product {
  shippingCost(
    package: PackageInput
      @require(field: "{ weight, dimension: dimension.{ width, height } }")
  ): Currency
}

6.2Language

According to the GraphQL specification, an argument is a key-value pair in which the key is the name of the argument and the value is a Value.

The Value of an argument can take various forms: it might be a scalar value (such as Int, Float, String, Boolean, Null, or Enum), a list (ListValue), an input object (ObjectValue), or a Variable.

Within the scope of the FieldSelectionMap, the relationship between input and output is established by defining the Value of the argument as a selection of fields from the output object.

Yet only certain types of Value have a semantic meaning. ObjectValue and ListValue are used to define the structure of the value. Scalar values, on the other hand, do not carry semantic importance in this context.

While variables may have legitimate use cases, they are considered out of scope for the current discussion.

However, it’s worth noting that there could be potential applications for allowing them in the future.

Given that these potential values do not align with the standard literals defined in the GraphQL specification, a new literal called SelectedValue is introduced, along with SelectedObjectValue.

Beyond these literals, an additional literal called Path is necessary.

6.2.1Name

Is equivalent to the Name defined in the GraphQL specification

6.2.2Path

PathSegment
FieldNameArgumentsConstopt
FieldNameArgumentsConstopt.PathSegment
FieldNameArgumentsConstopt<TypeName>.PathSegment
FieldName
Name
TypeName
Name

The Path literal is a string used to select a single output value from the return type by specifying a path to that value. This path is defined as a sequence of field names, each separated by a period (.) to create segments.

Example № 290book.title

Each segment specifies a field in the context of the parent, with the root segment referencing a field in the return type of the query.

A segment MAY include Arguments to disambiguate or parameterize the selected field, using the same syntax defined by the GraphQL specification. Because Arguments are matched as ArgumentsConst, only literal values are permitted — variables are not allowed in a Path.

In the following example, the width and height output fields are selected with the unit argument set to IMPERIAL:

Example № 291type Product {
  width(unit: Unit!): Float!
  height(unit: Unit!): Float!
  shippingCost(
    dimensions: DimensionInput
      @require(
        field: "{ width: width(unit: IMPERIAL), height: height(unit: IMPERIAL) }"
      )
  ): Currency
}

Arguments may also appear on intermediate segments of a Path:

Example № 292type Product {
  shippingCost(
    weight: Float @require(field: "packaging(material: BOX).weight")
  ): Currency
}

To select a field when dealing with abstract types, the segment selecting the parent field must specify the concrete type of the field using angle brackets after the field name if the field is not defined on an interface.

In the following example, the path mediaById<Book>.isbn specifies that mediaById returns a Book, and the isbn field is selected from that Book.

Example № 293mediaById<Book>.isbn

6.2.3SelectedValue

A SelectedValue consists of one or more SelectedValueEntry components, which may be joined by a pipe (|) operator to indicate alternative selections based on type.

Each SelectedValueEntry may take one of the following forms:

  • A Path (when not immediately followed by a dot) that is designed to point to a single value, although it may reference multiple fields depending on its return type.
  • A Path immediately followed by a dot and a SelectedObjectValue to denote a nested object selection.
  • A Path immediately followed by a SelectedListValue to denote selection from a list.
  • A standalone SelectedObjectValue

In the following example, the value could be title when referring to a Book and movieTitle when referring to a Movie.

Example № 294mediaById<Book>.title | mediaById<Movie>.movieTitle

The | operator can be used to match multiple possible SelectedValue. This operator is applied when mapping an abstract output type to a @oneOf input type.

Example № 295{ bookId: <Book>.id } | { movieId: <Movie>.id }
Example № 296{ nested: { bookId: <Book>.id } | { movieId: <Movie>.id } }

6.2.4SelectedObjectValue

SelectedObjectField
NameArgumentsConstopt

SelectedObjectValue are unordered lists of keyed input values wrapped in curly-braces {}. It has to be used when the expected input type is an object type.

When the shorthand form (without an explicit label) is used, the Name refers to both the input field and the output field of the same name. Arguments MAY follow the Name to parameterize the selected output field. As in Path, arguments use ArgumentsConst and therefore must consist of literal values only — variables are not allowed.

The following example uses the shorthand form together with arguments to select width and height with the unit argument:

Example № 297type Product {
  shippingCost(
    dimensions: DimensionInput
      @require(
        field: "dimensions[{ width(unit: IMPERIAL), height(unit: IMPERIAL) }]"
      )
  ): Currency
}

This structure is similar to the ObjectValue defined in the GraphQL specification, but it differs by allowing the inclusion of Path values within a SelectedValue, thus extending the traditional ObjectValue capabilities to support direct path selections.

A SelectedObjectValue following a Path is scoped to the type of the field selected by the Path. This means that the root of all SelectedValue inside the selection is no longer scoped to the root (defined by @is or @require) but to the field selected by the Path. The Path does not affect the structure of the input type.

This allows for reducing repetition in the selection.

Commas are optional throughout GraphQL so trailing commas are allowed and repeated commas do not represent missing values.

The following example is valid:

Example № 298type Product {
  dimension: Dimension!
  shippingCost(
    dimension: DimensionInput! @require(field: "dimension.{ size, weight }")
  ): Int!
}

The following example is equivalent to the previous one:

Example № 299type Product {
  dimension: Dimension!
  shippingCost(
    dimension: DimensionInput!
      @require(field: "{ size: dimension.size, weight: dimension.weight }")
  ): Int!
}

6.2.5SelectedListValue

A SelectedListValue is an ordered list of SelectedValue wrapped in square brackets []. It is used to express semantic equivalence between an argument expecting a list of values and the values of a list field within the output object.

The SelectedListValue differs from the ListValue defined in the GraphQL specification by only allowing one SelectedValue as an element.

The following example is valid:

Example № 300type Product {
  parts: [Part!]!
  partIds(partIds: [ID!]! @require(field: "parts[id]")): [ID!]!
}

In this example, the partIds argument is semantically equivalent to the id fields of the parts list.

The following example is invalid because it uses multiple SelectedValue as elements:

Counter Example № 301type Product {
  parts: [Part!]!
  partIds(parts: [PartInput!]! @require(field: "parts[id, name]")): [ID!]!
}

input PartInput {
  id: ID!
  name: String!
}

A SelectedObjectValue can be used as an element of a SelectedListValue to select multiple object fields as long as the input type is a list of structurally equivalent objects.

Similar to SelectedObjectValue, a SelectedListValue following a Path is scoped to the type of the field selected by the Path. This means that the root of all SelectedValue inside the selection is no longer scoped to the root (defined by @is or @require) but to the field selected by the Path. The Path does not affect the structure of the input type.

The following example is valid:

Example № 302type Product {
  parts: [Part!]!
  partIds(parts: [PartInput!]! @require(field: "parts[{ id, name }]")): [ID!]!
}

input PartInput {
  id: ID!
  name: String!
}

In case the input type is a nested list, the shape of the input object must match the shape of the output object.

Example № 303type Product {
  parts: [[Part!]]!
  partIds(
    parts: [[PartInput!]]! @require(field: "parts[[{ id, name }]]")
  ): [ID!]!
}

input PartInput {
  id: ID!
  name: String!
}

The following example is valid:

Example № 304type Query {
  findLocation(
    location: LocationInput!
      @is(field: "{ coordinates: coordinates[{ lat: x, lon: y }]}")
  ): Location @lookup
}

type Coordinate {
  x: Int!
  y: Int!
}

type Location {
  coordinates: [Coordinate!]!
}

input PositionInput {
  lat: Int!
  lon: Int!
}

input LocationInput {
  coordinates: [PositionInput!]!
}

6.3Validation

Validation ensures that FieldSelectionMap scalars are semantically correct within the given context.

Validation of FieldSelectionMap scalars occurs during the composition phase, ensuring that all FieldSelectionMap entries are syntactically correct and semantically meaningful relative to the context.

Composition is only possible if the FieldSelectionMap is validated successfully. An invalid FieldSelectionMap results in undefined behavior, making composition impossible.

In this section, we will assume the following type system in order to demonstrate examples:

type Query {
  mediaById(mediaId: ID!): Media
  findMedia(input: FindMediaInput): Media
  searchStore(search: SearchStoreInput): [Store]!
  storeById(id: ID!): Store
}

type Store {
  id: ID!
  city: String!
  media: [Media!]!
}

interface Media {
  id: ID!
}

type Book implements Media {
  id: ID!
  title: String!
  isbn: String!
  author: Author!
}

type Movie implements Media {
  id: ID!
  movieTitle: String!
  releaseDate: String!
}

type Author {
  id: ID!
  books: [Book!]!
}

input FindMediaInput @oneOf {
  bookId: ID
  movieId: ID
}

input SearchStoreInput {
  city: String
  hasInStock: FindMediaInput
}

input Nested {
  nested: FindMediaInput
}

6.3.1Path Field Selections

Each segment of a Path must correspond to a valid field defined on the current type context.

Formal Specification
  • For each segment in the Path:
    • If the segment is a field
      • Let fieldName be the field name in the current segment.
      • fieldName must be defined on the current type in scope.
Explanatory Text

The Path literal is used to reference a specific output field from an input field. Each segment in the Path must correspond to a field that is valid within the current type scope.

For example, the following Path is valid in the context of Book:

Example № 305title
Example № 306<Book>.title

Incorrect paths where the field does not exist on the specified type are not valid and result in validation errors. For instance, if <Book>.movieId is referenced but movieId is not a field of Book, it will result in an invalid Path.

Counter Example № 307movieId
Counter Example № 308<Book>.movieId

6.3.2Path Field Argument Validity

Each Arguments provided on a Path segment or on the shorthand form of a SelectedObjectField must be valid for the selected field, and every required argument of a selected field that is not annotated with @require must be provided.

Formal Specification
  • For each segment in the Path:
    • If segment is a type reference:
      • Continue
    • Let field be the field referenced by segment.
    • Let argumentDefinitions be the set of argument definitions of field.
    • Let arguments be the set of Arguments provided by segment, or the empty set if segment provides no Arguments.
    • For each argument in arguments:
      • Let argumentName be the Name of argument.
      • Let argumentDefinition be the argument definition in argumentDefinitions named argumentName.
      • argumentDefinition must exist.
      • Let value be the Value of argument.
      • value must not contain a Variable.
      • value must be coercible to the type of argumentDefinition.
    • For each argumentDefinition in argumentDefinitions:
      • If argumentDefinition is annotated with @require:
        • Continue
      • Let type be the expected type of argumentDefinition.
      • Let defaultValue be the default value of argumentDefinition.
      • If type is Non-Null and defaultValue does not exist:
        • Let argumentName be the name of argumentDefinition.
        • An argument in arguments named argumentName must exist.
  • The same rules apply to each SelectedObjectField that uses the shorthand form, where the field is the output field of the same name.
Explanatory Text

Arguments included on a field selection must be defined on the selected field and must coerce to the corresponding argument types. Every required argument of a selected field must be provided, even when the selection includes no Arguments at all. Arguments annotated with @require are exempt: their values are supplied by the executor, never by a selection map. Variables are not permitted; only literal values may appear.

The following example is valid because unit is a defined argument of width and IMPERIAL is a valid value of the Unit enum:

Example № 309type Product {
  width(unit: Unit!): Float!
  shippingCost(width: Float @require(field: "width(unit: IMPERIAL)")): Currency
}

The following example is invalid because scale is not a defined argument of width:

Counter Example № 310type Product {
  width(unit: Unit!): Float!
  shippingCost(width: Float @require(field: "width(scale: IMPERIAL)")): Currency
}

The following example is invalid because the required unit argument is not provided:

Counter Example № 311type Product {
  width(unit: Unit!): Float!
  shippingCost(width: Float @require(field: "width")): Currency
}

The following example is invalid because variables are not permitted in FieldSelectionMap:

Counter Example № 312type Product {
  width(unit: Unit!): Float!
  shippingCost(width: Float @require(field: "width(unit: $unit)")): Currency
}

6.3.3Path Terminal Field Selections

Each terminal segment of a Path must follow the rules regarding whether the selected field is a leaf node.

Formal Specification
  • For each segment in the Path:
    • Let selectedType be the unwrapped type of the current segment.
    • If selectedType is a scalar or enum:
      • There must not be any further segments in Path.
    • If selectedType is an object, interface, or union:
      • There must be another segment in Path.
Explanatory Text

A Path that refers to scalar or enum fields must end at those fields. No further field selections are allowed after a scalar or enum. On the other hand, fields returning objects, interfaces, or unions must continue to specify further selections until you reach a scalar or enum field.

For example, the following Path is valid if title is a scalar field on the Book type:

Example № 313title

The following Path is invalid because title should not have subselections:

Counter Example № 314title.something

For non-leaf fields, the Path must continue to specify subselections until a leaf field is reached:

Example № 315author.id

Invalid Path where non-leaf fields do not have further selections:

Counter Example № 316author

6.3.4Type Reference Is Possible

Each segment of a Path that references a type, must be a type that is valid in the current context.

Formal Specification
  • For each segment in a Path:
    • If segment is a type reference:
      • Let type be the type referenced in the segment.
      • Let parentType be the type of the parent of the segment.
      • Let applicableTypes be the intersection of GetPossibleTypes(type) and GetPossibleTypes(parentType).
      • applicableTypes must not be empty.
GetPossibleTypes(type)
  1. If type is an object type, return a set containing type.
  2. If type is an interface type, return the set of types implementing type.
  3. If type is a union type, return the set of possible types of type.
Explanatory Text

Type references inside a Path must be valid within the context of the surrounding type. A type reference is only valid if the referenced type could logically apply within the parent type.

6.3.5Values of Correct Type

Formal Specification
  • For each SelectedValue value:
    • Let type be the type expected in the position value is found.
    • value must be coercible to type.
Explanatory Text

Literal values must be compatible with the type expected in the position they are found.

The following examples are valid use of value literals in the context of FieldSelectionMap scalar:

Example № 317type Query {
  storeById(id: ID! @is(field: "id")): Store! @lookup
}

type Store {
  id: ID
  city: String!
}

Non-coercible values are invalid. The following example is invalid:

Counter Example № 318type Query {
  storeById(id: ID! @is(field: "id")): Store! @lookup
}

type Store {
  id: Int
  city: String!
}

6.3.6Selected Object Field Names

Formal Specification
  • For each Selected Object Field field in the document:
    • Let fieldName be the Name of field.
    • Let fieldDefinition be the field definition provided by the parent selected object type named fieldName.
    • fieldDefinition must exist.
Explanatory Text

Every field provided in an selected object value must be defined in the set of possible fields of that input object’s expected type.

For example, the following is valid:

Example № 319type Query {
  storeById(id: ID! @is(field: "id")): Store! @lookup
}

type Store {
  id: ID
  city: String!
}

In contrast, the following is invalid because it uses a field “address” which is not defined on the expected type:

Counter Example № 320type Query {
  storeById(id: ID! @is(field: "address")): Store! @lookup
}

type Store {
  id: ID
  city: String!
}

6.3.7Selected Object Field Uniqueness

Formal Specification
  • For each selected object value selectedObject:
    • For every field in selectedObject:
      • Let name be the Name of field.
      • Let fields be all Selected Object Fields named name in selectedObject.
      • fields must be the set containing only field.
Explanatory Text

Selected objects must not contain more than one field with the same name, as it would create ambiguity and potential conflicts.

For example, the following is invalid:

Counter Example № 321type Query {
  storeById(id: ID! @is(field: "{ id, id }")): Store! @lookup
}

type Store {
  id: ID
  city: String!
}

6.3.8Required Selected Object Fields

Formal Specification
  • For each Selected Object:
    • Let fields be the fields provided by that Selected Object.
    • Let fieldDefinitions be the set of input object field definitions of that Selected Object.
    • For each fieldDefinition in fieldDefinitions:
      • Let type be the expected type of fieldDefinition.
      • Let defaultValue be the default value of fieldDefinition.
      • If type is Non-Null and defaultValue does not exist:
        • Let fieldName be the name of fieldDefinition.
        • Let field be the input object field in fields named fieldName.
        • field must exist.
Explanatory Text

Input object fields may be required. This means that a selected object field is required if the corresponding input field is required. Otherwise, the selected object field is optional.

For instance, if the UserInput type requires the id field:

Example № 322input UserInput {
  id: ID!
  name: String!
}

Then, an invalid selection would be missing the required id field:

Counter Example № 323type Query {
  userById(user: UserInput! @is(field: "{ name: name }")): User! @lookup
}

If the UserInput type has an optional name field, but the User type requires the name field, the following selection would be valid.

Example № 324type Query {
  findUser(input: UserInput! @is(field: "{ name: name }")): User! @lookup
}

type User {
  id: ID
  name: String!
}

input UserInput {
  id: ID
  name: String
}

But if the UserInput type requires the name field but it’s not defined in the User type, the selection would be invalid.

Counter Example № 325type Query {
  findUser(input: UserInput! @is(field: "{ id: id }")): User! @lookup
}

type User {
  id: ID
}

input UserInput {
  id: ID
  name: String!
}

§Index

  1. AreTypesConsistent
  2. ArgumentsAreMergeable
  3. CollectOverriddenDeclarations
  4. CollectOverrideTargets
  5. ContractFieldDeclarations
  6. ContributedFields
  7. ContributingDeclarations
  8. EnumsAreMergeable
  9. FieldName
  10. FieldsAreMergeable
  11. GetPossibleTypes
  12. HasProvidesDirective
  13. InputFieldsAreMergeable
  14. InputFieldsHaveConsistentDefaults
  15. IsArgumentMappable
  16. IsEligibleOwnerDeclaration
  17. IsImplementationFieldType
  18. IsListType
  19. IsOutputSupertype
  20. IsShareableDeclaration
  21. IsValidKeyField
  22. KeyFieldsHasArguments
  23. LeastRestrictiveNamedOutputType
  24. LeastRestrictiveType
  25. MergeArgumentDefinitions
  26. MergeArguments
  27. MergeContractFields
  28. MergeEnumTypes
  29. MergeEnumValues
  30. MergeInputFields
  31. MergeInputTypes
  32. MergeInterfaceImplementations
  33. MergeInterfaceTypes
  34. MergeObjectTypes
  35. MergeOutputFields
  36. MergeProjectedOutputFields
  37. MergeScalarTypes
  38. MergeSchemas
  39. MergeTypes
  40. MergeUnionTypes
  41. MostRestrictiveType
  42. Path
  43. PathSegment
  44. ProjectInterfaceObjectFields
  45. ProvidesHasArguments
  46. ResolveSelectionCases
  47. SelectedListValue
  48. SelectedObjectField
  49. SelectedObjectValue
  50. SelectedValue
  51. SelectedValueEntry
  52. TypeName
  53. ValidateArgumentDefaultValues
  54. ValidateDefaultValue
  55. ValidateFieldSelectionSet
  56. ValidateInputFieldDefaultValues
  1. 1Overview
  2. 2Source Schema
    1. 2.1Entities and Identity
    2. 2.2@lookup
    3. 2.3@internal
    4. 2.4@inaccessible
    5. 2.5@is
    6. 2.6@require
    7. 2.7@key
    8. 2.8@interfaceObject
    9. 2.9@shareable
    10. 2.10@provides
    11. 2.11@external
    12. 2.12@override
  3. 3Schema Composition
    1. 3.1Validate Source Schemas
      1. 3.1.1Validate Type System
        1. 3.1.1.1Invalid GraphQL
        2. 3.1.1.2Disallowed Inaccessible Elements
        3. 3.1.1.3Type Definition Invalid
        4. 3.1.1.4Query Root Type Inaccessible
        5. 3.1.1.5Root Mutation Used
        6. 3.1.1.6Root Query Used
        7. 3.1.1.7Root Subscription Used
      2. 3.1.2Validate Internal Directives
        1. 3.1.2.1Internal Override Collision
      3. 3.1.3Validate External Directives
        1. 3.1.3.1External Unused
        2. 3.1.3.2External Override Collision
        3. 3.1.3.3External Provides Collision
        4. 3.1.3.4External Require Collision
        5. 3.1.3.5External on Interface
      4. 3.1.4Validate `@is` Directive
        1. 3.1.4.1Is Invalid Syntax
        2. 3.1.4.2Is Invalid Field Type
        3. 3.1.4.3Is Invalid Usage
        4. 3.1.4.4Is Fields Has Arguments
      5. 3.1.5Validate Key Directives
        1. 3.1.5.1Key Fields Select Invalid Type
        2. 3.1.5.2Key Directive in Fields Argument
        3. 3.1.5.3Key Fields Has Arguments
        4. 3.1.5.4Key Invalid Syntax
        5. 3.1.5.5Key Invalid Fields
        6. 3.1.5.6Key Invalid Fields Type
        7. 3.1.5.7Interface Object Key Missing
      6. 3.1.6Validate Lookup Directives
        1. 3.1.6.1Lookup Must Have Arguments
        2. 3.1.6.2Lookup Returns Non-Nullable Type
        3. 3.1.6.3Lookup Returns List
        4. 3.1.6.4Lookup Key Missing For Type
      7. 3.1.7Validate Override Directives
        1. 3.1.7.1Override from Self
        2. 3.1.7.2Override on Interface
      8. 3.1.8Validate Provides Directives
        1. 3.1.8.1Provides Directive in Fields Argument
        2. 3.1.8.2Provides Fields Has Arguments
        3. 3.1.8.3Provides Fields Missing External
        4. 3.1.8.4Provides Invalid Syntax
        5. 3.1.8.5Provides Invalid Fields
        6. 3.1.8.6Provides Invalid Fields Type
        7. 3.1.8.7Provides on Non-Composite Field
      9. 3.1.9Validate Require Directives
        1. 3.1.9.1Require Invalid Syntax
        2. 3.1.9.2Require Invalid Fields Type
        3. 3.1.9.3Require Invalid Usage
        4. 3.1.9.4Require Inconsistent on Implementation
      10. 3.1.10Validate Shareable Directives
        1. 3.1.10.1Invalid Shareable Usage
    2. 3.2Pre Merge Validation
      1. 3.2.1Validate Type System
        1. 3.2.1.1Type Kind Mismatch
      2. 3.2.2Validate Enums
        1. 3.2.2.1Enum Values Mismatch
      3. 3.2.3Validate Composite Types
        1. 3.2.3.1Output Field Types Mergeable
        2. 3.2.3.2Field Argument Types Mergeable
        3. 3.2.3.3Field With Missing Required Arguments
      4. 3.2.4Validate Input Types
        1. 3.2.4.1Input Field Default Mismatch
        2. 3.2.4.2Input Field Types mergeable
        3. 3.2.4.3Input With Missing Required Fields
      5. 3.2.5Validate External Directives
        1. 3.2.5.1External Missing on Base
        2. 3.2.5.2External Type Mismatch
      6. 3.2.6Validate Override Directives
        1. 3.2.6.1Override Source Has Override
      7. 3.2.7Validate Shareable Directives
        1. 3.2.7.1Invalid Field Sharing
      8. 3.2.8Validate Interface Object Directives
        1. 3.2.8.1Interface Object No Interface
        2. 3.2.8.2Interface Object Key Mismatch
    3. 3.3Merge
      1. 3.3.1Merge Scalar Types
      2. 3.3.2Merge Interface Types
      3. 3.3.3Merge Enum Types
      4. 3.3.4Merge Union Types
      5. 3.3.5Merge Input Types
      6. 3.3.6Merge Object Types
      7. 3.3.7Merge Interface Implementations
      8. 3.3.8Project Interface Object Fields
      9. 3.3.9Merge Output Fields
      10. 3.3.10Merge Input Fields
      11. 3.3.11Merge Argument Definitions
      12. 3.3.12Merge Arguments
      13. 3.3.13Shared Algorithms
        1. 3.3.13.1Least Restrictive Type
        2. 3.3.13.2Most Restrictive Type
    4. 3.4Post Merge Validation
      1. 3.4.1Validate Type System
        1. 3.4.1.1Invalid Merged GraphQL
        2. 3.4.1.2No Queries
        3. 3.4.1.3Reference To Inaccessible Type
        4. 3.4.1.4Reference To Internal Type
      2. 3.4.2Validate Composite Types
        1. 3.4.2.1Empty Merged Object Type
        2. 3.4.2.2Empty Merged Interface Type
        3. 3.4.2.3Implemented by Inaccessible
        4. 3.4.2.4Interface Field No Implementation
        5. 3.4.2.5Invalid Projected Field Sharing
        6. 3.4.2.6Interface Field Type Mismatch
        7. 3.4.2.7Interface Field Argument No Implementation
        8. 3.4.2.8Interface Field Argument Type Mismatch
      3. 3.4.3Validate Input Types
        1. 3.4.3.1Empty Merged Input Object Type
        2. 3.4.3.2Non-Null Input Fields cannot be inaccessible
      4. 3.4.4Validate Enums
        1. 3.4.4.1Empty Merged Enum Type
        2. 3.4.4.2Enum Type Default Value Inaccessible
      5. 3.4.5Validate Union Types
        1. 3.4.5.1Empty Merged Union Type
      6. 3.4.6Validate Is Directives
        1. 3.4.6.1Is Invalid Fields
      7. 3.4.7Validate Require Directives
        1. 3.4.7.1Require Invalid Fields
    5. 3.5Validate Satisfiability
      1. 3.5.1Unsatisfiable Query Path
      2. 3.5.2Resolve Selection Cases
  4. 4Executor
    1. 4.1Configuration
  5. 5Shared Types
    1. 5.1Name
    2. 5.2FieldSelection
  6. 6Appendix A: Specification of FieldSelectionMap Scalar
    1. 6.1Introduction
      1. 6.1.1Scope
    2. 6.2Language
      1. 6.2.1Name
      2. 6.2.2Path
      3. 6.2.3SelectedValue
      4. 6.2.4SelectedObjectValue
      5. 6.2.5SelectedListValue
    3. 6.3Validation
      1. 6.3.1Path Field Selections
      2. 6.3.2Path Field Argument Validity
      3. 6.3.3Path Terminal Field Selections
      4. 6.3.4Type Reference Is Possible
      5. 6.3.5Values of Correct Type
      6. 6.3.6Selected Object Field Names
      7. 6.3.7Selected Object Field Uniqueness
      8. 6.3.8Required Selected Object Fields
  7. §Index