Value types in Apollo Federation
Share types and fields across multiple subgraphs
⚠️ The field sharing model has changed significantly in Federation 2. For a summary of these changes, see what's new.
For more information about using value types in Federation 1, you can view the previous version of this article.
In a federated graph, it's common to want to reuse a GraphQL type across multiple subgraphs.
For example, suppose you want to define and reuse a generic Position
type in different subgraphs:
type Position {x: Int!y: Int!}
Types like this are called value types. This article describes how to share value types and their fields in federated graph, enabling multiple subgraphs to define and resolve them.
Sharing object types
By default in Federation 2 subgraphs, a single object field can't be defined or resolved by more than one subgraph schema.
Consider the following Position
example:
❌
type Position {x: Int!y: Int!}
type Position {x: Int!y: Int!}
Attempting to compose these two subgraph schemas together will break composition. The gateway doesn't know which subgraph is responsible for resolving Position.x
and Position.y
. To enable multiple subgraphs to resolve these fields, you must first mark that field as @shareable
.
As an alternative, if you want Subgraphs A and B to resolve different fields of Position
, you can designate the Position
type as an entity.
Using @shareable
The @shareable
directive enables multiple subgraphs to resolve a particular object field (or set of object fields).
To use @shareable
in a subgraph schema, you first need to add the following snippet to that schema to opt in to Federation 2:
extend schema@link(url: "https://specs.apollo.dev/federation/v2.0",import: ["@key", "@shareable"])
Then you can apply the @shareable
directive to an object type, or to individual fields of that type:
✅
type Position @shareable {x: Int!y: Int!}
type Position {x: Int! @shareabley: Int! @shareable}
Marking a type as @shareable
is equivalent to marking all of its fields as @shareable
, so the two subgraph definitions above are equivalent.
Both subgraphs A and B can now resolve the x
and y
fields for the Position
type, and our subgraph schemas will successfully compose into a supergraph schema.
⚠️ Important considerations for @shareable
- If a type or field is marked
@shareable
in any subgraph, it must be marked@shareable
or@external
in every subgraph that defines it. Otherwise, composition fails. - If multiple subgraphs can resolve a field, make sure each subgraph's resolver for that field behaves identically. Otherwise, queries might return inconsistent results depending on which subgraph resolves the field.
Differing shared fields
Shared fields can differ between subgraphs in specific ways:
- The return type of a shared field can vary in nullability (
String
/String!
).- A shared field's return type can't vary in its core type (
Int
vs.String
) or in whether it returns a list (Int
vs.[Int]
).
- A shared field's return type can't vary in its core type (
- A field can be omitted from a subgraph, IF that field is always resolvable.
For example, take a look at the shared Food
type below:
type Food @shareable {name: String!price: Int!}
type Food @shareable {name: String!price: Int # NullableinStock: Boolean! # Not in A}
The above Food
types differ in the nullability of their fields and the fields included in each type.
Return types
Let's say two subgraphs both define an Event
object type with a timestamp
field:
❌
type Event @shareable {timestamp: Int!}
type Event @shareable {timestamp: String!}
Subgraph A's timestamp
returns an Int
, and Subgraph B's returns a String
. This is invalid. When composition attempts to generate an Event
type for the supergraph schema, it fails due to an unresolvable conflict between the two timestamp
field definitions.
Next, look at these varying definitions for the Position
object type:
✅
type Position @shareable {x: Int!y: Int!}
type Position @shareable {x: Inty: Int}
The x
and y
fields are non-nullable in Subgraph A, but they're nullable in Subgraph B. This is valid! Composition recognizes that it can use the following definition for Position
in the supergraph schema:
type Position {x: Inty: Int}
This definition works for querying Subgraph A, because Subgraph A's definition is more restrictive than this (a non-nullable value is always valid for a nullable field). In this case, composition coerces Subgraph A's Position
fields to satisfy the reduced restrictiveness of Subgraph B.
Note that Subgraph A's actual subgraph schema is not modified. Within Subgraph A, x
and y
remain non-nullable.
Arguments
Arguments for a shared field can differ between subgraphs in certain ways:
- If an argument is required in at least one subgraph, it can be optional in other subgraphs. It cannot be omitted.
- If an argument is optional in every subgraph where it's defined, it can be omitted in other subgraphs. However:
- ⚠️ If a field argument is omitted from any subgraph, that argument is omitted from the supergraph schema entirely! This means that clients can't provide the argument for that field.
✅
type Building @shareable {# Argument is requiredheight(units: String!): Int!}
type Building @shareable {# Argument can be optionalheight(units: String): Int!}
❌
type Building @shareable {# Argument is requiredheight(units: String!): Int!}
type Building @shareable {# ⚠️ Argument can't be omitted! ⚠️height: Int!}
⚠️
type Building @shareable {# Argument is optionalheight(units: String): Int!}
type Building @shareable {# Argument can be omitted, BUT# it doesn't appear in the# supergraph schema!height: Int!}
For more information, see Input types and field arguments.
Omitting fields
Look at these two definitions of a Position
object type:
⚠️
type Position @shareable {x: Int!y: Int!}
type Position @shareable {x: Int!y: Int!z: Int!}
Subgraph B defines a z
field, but Subgraph A doesn't. In this case, when composition generates the Position
type for the supergraph schema, it includes all three fields:
type Position {x: Int!y: Int!z: Int!}
This definition works for Subgraph B, but it presents a problem for Subgraph A. Let's say Subgraph A defines the following Query
type:
type Query {currentPosition: Position!}
According to the hypothetical supergraph schema, the following query is valid against the supergraph:
❌
query GetCurrentPosition {currentPosition {xyz # ⚠️ Unresolvable! ⚠️}}
And here's the problem: if Subgraph B doesn't define Query.currentPosition
, this query must be executed on Subgraph A. But Subgraph A is missing the Position.z
field, so that field is unresolvable!
Composition recognizes this potential problem, and it fails with an error. So how do we fix it? Check out Solutions for unresolvable fields.
Adding new shared fields
Adding a new field to a value type can cause composition issues, because it's challenging to add the field to all defining subgraphs at the same time.
Let's say we're adding a z
field to our Position
value type, and we start with Subgraph A:
⚠️
type Position @shareable {x: Int!y: Int!z: Int!}
type Position @shareable {x: Int!y: Int!}
It's likely that when we attempt to compose these two schemas, composition will fail, because Subgraph B can't resolve Position.z
.
To incrementally add the field to all of our subgraphs without breaking composition, we can use the @inaccessible
directive.
Using @inaccessible
If you apply the @inaccessible
directive to a field, composition omits that field from your gateway's API schema. This helps you incrementally add a field to multiple subgraphs without breaking composition.
To use @inaccessible
in a subgraph, first make sure you include it in the import
array of your Federation 2 opt-in declaration:
extend schema@link(url: "https://specs.apollo.dev/federation/v2.0",import: ["@key", "@shareable", "@inaccessible"])
Then, whenever you add a new field to a value type, apply @inaccessible
to that field if it isn't yet present in every subgraph that defines the value type:
type Position @shareable {x: Int!y: Int!z: Int! @inaccessible}
type Position @shareable {x: Int!y: Int!}
Even if Position.z
is defined in multiple subgraphs, you only need to apply @inaccessible
in one subgraph to omit it. In fact, you might want to apply it in only one subgraph to simplify removing it later.
With the syntax above, composition omits Position.z
from the generated API schema, and the resulting Position
type includes only x
and y
fields.
Note that Position.z
does appear in the supergraph schema, but the API schema enforces which fields clients can include in operations. Learn more about federated schemas.
Whenever you're ready, you can now add Position.z
to Subgraph B:
type Position @shareable {x: Int!y: Int!z: Int! @inaccessible}
type Position @shareable {x: Int!y: Int!z: Int!}
At this point, Position.z
is still @inaccessible
, so composition continues to ignore it.
Finally, when you've added Position.z
to every subgraph that defines Position
, you can remove @inaccessible
from Subgraph A:
type Position @shareable {x: Int!y: Int!z: Int!}
type Position @shareable {x: Int!y: Int!z: Int!}
Composition now successfully includes Position.z
in the supergraph schema!
Unions, interfaces, and inputs
In Federation 2, interface
and union
type definitions can be shared between subgraphs by default, and those definitions can differ:
union Media = Book | Movieinterface User {name: String!}
union Media = Book | Podcastinterface User {name: String!age: Int!}
Compositional logic merges these definitions in your supergraph schema:
union Media = Book | Movie | Podcast# The object types that implement this interface are# responsible for resolving these fields.interface User {name: String!age: Int!}
This can be useful when different subgraphs are responsible for different subsets of a particular set of related types or values.
You can also use the enum
type across multiple subgraphs. For details, see Merging types from multiple subgraphs.
Subgraphs can share input
type definitions, but composition merges them differently. When input
types are composed across multiple subgraphs, only their mutual fields are carried over into the supergraph schema:
input UserInput {name: String!age: Int}
input UserInput {name: String!email: String}
Compositional logic merges only the fields that all input
types have in common. To learn more, see Merging input types and field arguments.
input UserInput {name: String!}
To learn more about how composition merges different schema types under the hood, see Merging types during composition.