When a GraphQL field returns an interface or union, how does the server decide the concrete object type?
answer
- The schema declares shape, not instance
- Something must decide during execution
- One decision per resolved value
- It must name a possible type
- Nothing matches means a field error
basics
~10 sThe schema never says which one, so the executor asks the type system at run time, once per resolved value. That hook must name exactly one object type from the abstract type's possible types.
solid answer
~50 sAn interface or union field promises a value but not which object type it will be, and the executor cannot go any further without knowing: the fields that exist, the field resolvers that run and the fragments that contribute all depend on the concrete type. So value completion for an abstract type has an extra step in front of it. The specification models it as an internal type-resolution function the **type system** supplies: it receives the abstract type and the value the field resolver produced, and it must return an object type that is one of that abstract type's *possible types* — the union's listed members, or every object type declaring it implements the interface. The executor then collects and executes the sub-selection against that object type. It runs per resolved value, not per field, so a donations feed returning 4,312 contributions runs it 4,312 times. If nothing names a type, the specification says raise a field error.
code
graphql · 26 linesinterface Contribution {
id: ID!
amountMinor: Int!
}
type OneOffGift implements Contribution {
id: ID!
amountMinor: Int!
giftAidClaimed: Boolean!
}
type RecurringPledge implements Contribution {
id: ID!
amountMinor: Int!
monthlyOn: Int!
}
type GrantAward implements Contribution {
id: ID!
amountMinor: Int!
awardingBody: String!
}
type Query {
contributions(campaignId: ID!): [Contribution!]!
}go deeper
Recall that an interface or union field needs a run-time decision the schema cannot make, that it produces exactly one object type from the abstract type's possible types, and that the decision happens once for every resolved value.
Be ready to explain why the decision must precede the sub-selection: fields, field resolvers and applicable fragments all depend on the concrete type. Know that failing to name one raises a field error rather than yielding an empty object.
Show that you know the per-value cost and where it bites — large lists, deep documents, expensive discrimination — and that an unresolved value surfaces as a field error inside a 200 response that monitoring on status codes alone will never see.
Own the policy question: whether abstract types are worth their run-time discrimination cost at all, where in the schema they earn it, and what the team's standard is for keeping resolution total as the possible-type set grows.
## What "abstract" costs the executor Interfaces and unions are GraphQL's two abstract types, and they are the only declared return types that do not answer the executor's question. A field declared `Money!` returns a `Money`. A field declared `Contribution`, where `Contribution` is an interface implemented by `OneOffGift`, `RecurringPledge` and `GrantAward`, returns one of those three — and the schema is deliberately silent about which. That silence stops execution dead at the sub-selection, because three separate things hang off the concrete type: * **Which fields exist.** An implementing type may declare fields the interface never did; a union member shares no fields with its siblings at all. * **Which field resolvers run.** Two implementers can resolve the same interface-declared field in completely different ways — `amountMinor` might be a stored column on one type and a computed sum on another. * **Which fragments contribute anything.** A fragment written on `RecurringPledge` contributes no fields to a value that is really a `GrantAward`. So the executor has to settle the type before it can do any of the work. ## The step the specification defines The specification models this as an internal function that the **type system definition must provide** — not something the document, the client or the schema text can answer. It takes the abstract type and the value that the field's resolver returned, and it must return an **object type** that is a *possible type* of that abstract type. "Possible types" is a precise term: * for a **union**, exactly the members listed in its definition; * for an **interface**, every object type in the schema that declares it implements that interface. With an object type in hand, the executor collects the sub-selection against it — dropping the fragments that do not apply — and executes it exactly as it would for a field that had been declared to return that object type all along. The resolved name is also what the response reports as the value's type when the document asks for it. ## Two consequences people miss **It runs once per value, not once per field.** A `contributions` field returning a list of 4,312 items resolves a type 4,312 times, and each of those resolutions can land on a different object type. In a 19-level-deep reporting document with an interface at several levels, the step runs at every level, for every value. **It runs even when the client asks for nothing type-specific.** A document that selects only interface-declared fields still forces the resolution, because the executor must know whose field resolvers to invoke. Skipping it "because no fragment needs it" would silently execute the wrong code. ## What the hook is allowed to return Exactly one object type from the possible set. Not the interface. Not the union. Not a list of candidates for the client to narrow. And not an object type outside the set — an interface's implementer that the union does not list is not a member of that union, and returning it would produce a response shape the client's document was never validated against. How the hook decides is left entirely to the server: the runtime class of the value, a discriminator column on the fetched row, or a plain type name carried on the record are all common. All three are implementation convenience, not specification. ## When nothing matches If the hook cannot name a possible type, the specification's answer is a **field error** raised at that field. The ordinary field-error consequences follow: that position in `data` becomes null, or the failure propagates outward if the field is non-null, an entry naming the field's path appears in the `errors` list, and the sibling fields of the document still resolve normally. It is a server-side execution failure inside an otherwise successful response envelope, not a transport-level failure and not a client validation error — which is exactly why it can sit unnoticed in a low-traffic branch for weeks. ## Worked shape A charity donations graph with a mixed feed makes the whole thing concrete: a `contributions` field declared to return `[Contribution!]!`, three implementing types with genuinely different fields, and a response in which item 1 was resolved to `RecurringPledge` and item 2 to `GrantAward`. Nothing in the SDL and nothing in the query decided that. A function running during execution, once per item, did.
- If the document selects only fields declared on the interface, does the server still have to resolve the concrete type?Yes. The interface only declares that a field exists; each implementing object type supplies its own resolver for it. The executor cannot invoke a field resolver without knowing which object type it is executing against, so the resolution happens whether or not any fragment in the document cares about the answer.
- How many times does a server resolve a type for a list of 4,312 contributions?Once per item, so 4,312 times. Type resolution is part of completing an individual value, not of completing the list; each element is resolved independently and different elements may land on different object types. That per-value cost is why an expensive resolution strategy over a large list shows up as latency.
- Can the hook return an object type that is not among the abstract type's possible types?No. It must return one of the union's declared members, or one of the object types declaring they implement the interface. Anything else is treated as a failure to resolve, because the client's document was validated against the possible-type set and a foreign type would yield fields no fragment ever asked for.
A parcel labelled only "donation" cannot be routed; someone in the sorting room has to open it and stamp it "pledge" or "grant" before any of the type-specific handling can start.
saying these in an interview costs you the question
- Says the client's inline fragments tell the server the type
- Thinks the schema alone determines the concrete type
- Believes the hook may return the interface or union itself
- Says resolution runs once per field, not per value
- Assumes an unresolvable value returns an empty object, not an error
- Thinks resolution is skipped when only interface fields are selected