Are circular references between two object types legal in a GraphQL schema?
answer
- Fields name types, they do not embed them
- Schema is a graph, response is a tree
- The caller's document is finite text
- One construct really cannot be circular
- Nullable or list breaks an input chain
basics
~20 sYes. Object types refer to each other by name, so a schema is a graph and cycles — including a type referring to itself — are ordinary. What is finite is the document a caller writes, not the type graph.
solid answer
~50 sCycles between **object types** are legal and completely routine: `Vehicle.currentDriver: Driver` alongside `Driver.assignedVehicles: [Vehicle!]!` is a valid schema, and a self-reference such as `Vehicle.replacedBy: Vehicle` is valid too. Fields name their types rather than embedding them, so nothing needs to be expanded when the schema is built. Termination comes from the executable document: a caller writes a finite selection set, and execution walks exactly that. The one place the specification does forbid a cycle is **input object types** — an input object must not reference itself through a chain of fields that are all non-null and non-list, because no finite value could ever satisfy it; breaking the chain with a nullable or list field makes it legal. The practical cost of a cycle is not legality but design: a back-reference is a field you must resolve and keep correct, and deeply cyclic schemas make traversal amplification easy, which operators bound with request caps.
code
graphql · 10 linestype Vehicle {
id: ID!
currentDriver: Driver
replacedBy: Vehicle
}
type Driver {
id: ID!
assignedVehicles: [Vehicle!]!
}go deeper
Recall that types reference each other by name, so cycles and self-references are ordinary, and that the caller's finite selection set is what ends the walk. Knowing this stops a lot of confused schema design.
Explain the schema-graph versus response-tree distinction, and know the specification's one type-system exception: an input object may not cycle through fields that are all non-null and non-list.
Show the design half — a back-reference is a field with a resolver, a bound and a contract, so it needs a caller that starts from that side, and cyclic schemas make deep expensive documents easy to write.
Treat reflexive back-references as schema surface that costs review, tooling and support forever; decide as a matter of policy which directions of a relationship the platform actually promises.
## Why a cycle is unremarkable In SDL, a field declares the **name** of its type, not its contents: ```graphql type Vehicle { id: ID! plate: String! currentDriver: Driver replacedBy: Vehicle } type Driver { id: ID! name: String! assignedVehicles: [Vehicle!]! } ``` Nothing here needs to be expanded or inlined, so nothing recurses at schema-build time. The schema is a graph of named types with edges between them, and a graph is allowed to have cycles — that is what makes it a graph rather than a tree. `Vehicle` pointing at `Driver` while `Driver` points back is normal; `Vehicle.replacedBy: Vehicle` — the unit that took over a decommissioned one's route — is a self-loop and equally normal. The question trips people up because they carry an intuition from data formats. A JSON document, a Protobuf message, an XML schema instance — those are trees, and a cyclic definition would suggest an infinite value. GraphQL avoids this by separating two things that other formats fuse: the **schema**, which is a graph and may cycle, and the **response**, which is a tree. ## What actually terminates execution The response is a tree because the caller's selection set is a tree. A caller may write: ```graphql query { vehicle(id: "v-7731") { plate currentDriver { name assignedVehicles { plate } } } } ``` and may keep going around the loop as many times as they care to type. What they cannot do is write an infinite document — a document is finite text, and execution walks exactly the fields it selects. There is no rule in the specification that stops a cycle, because none is needed for termination. This has an operational consequence rather than a correctness one: a caller *can* type a very deep traversal, and each level multiplies the work behind it, so servers exposed to untrusted callers apply limits on what a single request may ask for. Those limits are an operational control, not part of the type system, and they exist precisely because a cyclic schema makes a deep document easy to write. ## The one construct where the specification bans a cycle Input object types are the exception, and knowing it is what separates a curious answer from a memorised one. Because an input value must be fully supplied by the caller, an input object that contains itself through a chain of fields that are **all non-null and none of them lists** could never be given a value — every value would require another one beneath it, forever. The specification therefore requires that any such reference cycle be broken by at least one field in the chain being nullable or a list: ```graphql # invalid: the cycle is entirely non-null and non-list input RouteFilter { and: RouteFilter! } # valid: nullable breaks the chain input RouteFilterNullable { and: RouteFilterNullable } # valid: a list breaks the chain, since an empty list terminates it input RouteFilterList { all: [RouteFilterList!]! } ``` The list case is the one people get wrong: `[RouteFilterList!]!` looks non-null all the way down, but an empty list is a perfectly good value, so the recursion can bottom out. This rule is why recursive filter inputs are always written with nullable or list-typed combinators. For completeness, there is a second cycle ban elsewhere in the specification, and it is about documents rather than the type system: named fragments may not spread each other in a cycle, since a fragment spread really does substitute its contents and a cycle would have no finite expansion. ## The design question hiding behind the legality question Because cycles are free, schemas accumulate them by reflex: someone adds `Driver.assignedVehicles` because `Vehicle.currentDriver` exists and symmetry feels tidy. Symmetry is not a reason. A back- reference is a real field — it needs a resolver, an ordering, a nullability decision, and a bound if it returns a collection — and once published it is part of the contract whether anyone uses it or not. The useful test is whether a screen or a caller actually starts from the far side. In a fleet telematics graph, dispatch starts from the vehicle and wants the driver, so `Vehicle.currentDriver` earns its place. A driver's own app starts from the driver and wants today's vehicle, so the reverse edge earns its place too. A third edge added "for completeness" — every telemetry sample pointing back at its vehicle, which pointed at the sample list to begin with — buys nothing and gives callers one more way to write an expensive document.
- If cycles are legal, what stops a caller walking one forever?Nothing in the type system — the document does. A caller must write out every level it wants, and text is finite, so execution walks exactly that selection and stops. Servers open to untrusted callers additionally cap how much a single request may ask for, but that is an operational control layered on top, not a rule of the specification.
- Why can an input object not contain itself through non-null fields?Because the caller has to supply a complete value. If every field in the cycle is non-null and none is a list, producing a value would require producing another one beneath it without end. The specification therefore requires at least one link in such a chain to be nullable or a list — an empty list, or an omitted nullable field, is where the value terminates.
- Should you add a back-reference just because the forward edge exists?No. Symmetry is not a requirement. Each direction is a field with its own resolver, ordering, nullability and — if it returns a collection — bound, and once published it is contract. Add the reverse edge when a real caller starts from that side, not for tidiness.
A street map may contain loops without being infinite. What ends a journey is the route you write down, not the map.
saying these in an interview costs you the question
- Says a schema must be acyclic like a tree
- Claims a cycle causes infinite recursion at execution
- Thinks a self-referencing object type is invalid SDL
- Believes input objects may cycle like object types
- Says a list-typed input cycle is illegal
- Adds back-references for symmetry with no caller