skip to content

Why can a recursive input object make a schema fail validation?

level: seniorimportance: nice to knowfreq 22%

answer

  1. Ask whether any value could satisfy it
  2. Every link in the chain required
  3. The obligation never terminates
  4. Two ways to give the caller an exit
  5. An empty list counts as one

basics

~20 s

A chain of Non-Null fields that returns to the same input object can never be satisfied by a finite value. The specification requires at least one link in such a cycle to be nullable or a list type.

solid answer

~40 s

Input objects may reference themselves; that is how nested filter arguments are built. What is invalid is a reference cycle in which **every** link is a Non-Null field that is neither a list nor defaulted, because writing a value for it would require infinite nesting — `parent: CaseFilterInput!` demands a parent, whose parent demands a parent, forever. Schema validation rejects that at build time rather than letting every request fail. Two things break the cycle: making one field in the chain nullable, so the caller may simply omit it; or making one a list, since an empty list terminates the recursion even when both the list and its elements are Non-Null. The rule is transitive — the cycle may run through several input objects before it closes.

code

graphql · 14 lines
graphql
input CaseFilterInput {
  courtId: ID!
  parent: CaseFilterInput!
}

input NullableRepair {
  courtId: ID!
  parent: NullableRepair
}

input ListRepair {
  courtId: ID!
  all: [ListRepair!]!
}

go deeper

for a junior

Know only that input objects are allowed to reference themselves — that is how nested filter arguments work — and that adding Non-Null to an input field forces every caller to supply it. The cycle rule itself is not expected at this level.

for a middle

Explain the mechanics: a chain of required self-references has no finite value, so schema validation rejects it, and either a nullable link or a list link makes it satisfiable again. Be able to spot the loop in a two-input-object example.

for a senior

Demonstrate the diagnosis: a service that stops building after someone tightened an input field, the type chain in the error, and the reasoning that Non-Null on input is an obligation on callers rather than a promise from the server.

for a principal

Own the boundary this rule does not cover — a valid recursive input still accepts arbitrarily deep values, so decide separately where incoming operations are bounded and who sets that limit for every service exposing a filter DSL.

## The rule, and the reason behind it GraphQL is happy for an input object to reference itself. Nested filter and predicate arguments — the `and`/`or`/`not` shape almost every search API grows — depend on it. What the type system forbids is a self-reference in which every link is **required**: a Non-Null field that is not a list and carries no default value. The argument is about satisfiability, and it is worth being able to state it in one sentence. Suppose a legal case-file graph declares: ```graphql input CaseFilterInput { courtId: ID! parent: CaseFilterInput! } ``` To send one value of `CaseFilterInput`, the caller must supply `parent`, because it is Non-Null. That parent is itself a `CaseFilterInput`, so it must supply its own `parent`. There is no depth at which the obligation ends. A client that nested nineteen levels of filter would have written a 19-level-deep document and still be one required field short of a legal value; so would a client that nested nineteen thousand. The type describes a value that cannot exist, and a schema that contains it would accept no request against that argument at all. So validation catches it once, at schema-build time, instead of producing a per-request coercion error that would look like a client mistake. The failure mode is the one every schema-validity rule shares: the server does not start. ## The two escape hatches The specification requires that, in any such reference cycle, at least one field be either **nullable** or a **list type**. Both work for the same underlying reason — each gives the caller a way to stop. **Nullable** is the obvious one. `parent: CaseFilterInput` may be omitted, and omission terminates the chain. **A list** is the interesting one, and it catches people out: `all: [CaseFilterInput!]!` is valid even though both the list and its elements are Non-Null, because the caller may pass `[]`. An empty list satisfies a Non-Null list, and an empty list contains no elements that demand further nesting. That is why the conventional predicate shape — `and: [FilterInput!]`, `or: [FilterInput!]` — has never been a problem. The rule is transitive. The cycle need not be direct: `CaseFilterInput` requiring a `PartyFilterInput!` which requires a `CaseFilterInput!` closes exactly the same loop, and validation follows the chain across as many input objects as it takes. ## Diagnosing it The symptom is a service that will not come up after what looked like a harmless SDL edit — typically somebody tightened a nullable field to Non-Null for "correctness" and closed a loop that had been open for months. The build error names the type chain, so the fix is mechanical: find the link the tightening broke and put the nullability back, or restate the recursion through a list. The design lesson is the one worth stating in an interview: **on an input object, a Non-Null field is not a free correctness win.** On an output field, Non-Null tightens a promise the server makes. On an input field, it is an obligation on every caller, and inside a recursive structure that obligation can become unsatisfiable. ## Valid is not the same as safe Breaking the cycle with a list makes the schema build, and it does nothing at all about size. A recursive filter typed `all: [CaseFilterInput!]!` will happily accept a value nested nineteen levels deep with a list that grew without a bound at each level, and every level may be translated into more backend work. Schema validation has no view on that: it is a static check over type definitions, with no notion of how large a value is or how expensive it is to serve. Bounding the depth and size of an incoming value is an execution-time concern handled elsewhere — limits applied before or during execution, not by the type system. That separation is the honest answer to "so does the cycle rule protect me from unbounded input?" No. It protects you from a type nobody could satisfy. Protection from a type somebody can satisfy far too enthusiastically is a different mechanism entirely, and conflating the two is the common mistake here. ## Where it sits in the spec This is a genuine type-system rule, not a convention: an implementation that served such a schema would be non-conforming. It is also one of the rules people meet only by tripping over it, which is why it makes a pleasant differentiator question rather than a screening one — the interviewer is checking whether you reason about satisfiability, not whether you memorised a clause.

  • Why does a Non-Null list of Non-Null elements break the cycle when a Non-Null field does not?
    Because an empty list satisfies a Non-Null list. The caller supplies `[]`, which contains no elements, so nothing inside demands further nesting and the recursion terminates. A plain Non-Null field offers no such empty value — the caller must supply exactly one more instance of the type, every time, forever.
  • Does this rule also apply to an object type that references itself through Non-Null fields?
    No, and the asymmetry is the point. An output field is *resolved*, not supplied: the client decides how deep to select, so a self-referencing Non-Null output field simply stops wherever the selection set stops. An input value has to be written out in full before the request is sent, which is why only input objects carry the cycle rule.
  • Once the cycle is broken with a list, what still bounds how deep a filter a client may send?
    Nothing in the type system. Schema validation is a static check over definitions and has no notion of value size, so a valid recursive filter accepts arbitrarily deep and wide input. Bounding it is an execution-time concern — limits applied to the incoming operation and its values before the work is done — and it must be chosen deliberately rather than inherited from the schema.

It is a form that requires you to attach a completed copy of the same form. Allowing 'attach none' or 'attach a list, possibly empty' makes it fillable; requiring exactly one attachment makes it impossible at any depth.

saying these in an interview costs you the question

  • Says input objects may never reference themselves
  • Thinks the error appears per request, not at build
  • Believes a Non-Null list also closes the cycle
  • Treats Non-Null on input fields as free correctness
  • Claims the rule bounds how deep input can nest
  • Misses that the cycle can span several input objects

context