skip to content

How do you make a GraphQL list field require at least one selective filter argument?

level: seniorimportance: nice to knowfreq 24%

answer

  1. Which rule can the type system state
  2. Always-required is not at-least-one
  3. Exactly-one is a different construct entirely
  4. Tightening an input field breaks existing documents
  5. Split the field or check at execution

basics

~20 s

The type system cannot express "at least one of these". Non-null makes a filter mandatory on every call, breaking existing documents. The options are separate fields with a required anchor, a required argument on a new field, or a documented runtime check.

solid answer

~50 s

GraphQL can express *always required* — a non-null argument or input field with no default — and, through the spec-draft one-of pattern, *exactly one*. It cannot express *at least one of these*, and there is no directive in the specification for it. So on a farm sensor graph where `sensorReadings(filter: SensorReadingFilter)` lets a caller match everything, tightening `SensorReadingFilter.stationId` from `ID` to `ID!` is the wrong lever: on an input field, requiring what used to be optional is the breaking direction, and every saved document that filtered only by date stops validating the moment the deploy lands. The workable choices are: publish the access paths as separate fields whose anchor argument is genuinely non-null; put a required anchor argument on a **new** field; or keep the fields nullable and reject an under-specified call at execution with a coded error, accepting that the rule is then invisible to introspection and to generated clients.

code

graphql · 13 lines
graphql
type Query {
  readingsByStation(
    stationId: ID!
    recordedAfter: DateTime
    recordedBefore: DateTime
  ): [SensorReading!]!

  readingsByMetricWindow(
    metric: MetricKind!
    recordedAfter: DateTime!
    recordedBefore: DateTime!
  ): [SensorReading!]!
}

go deeper

for a junior

Focus on the two things the type system can say about an argument: it is optional, or it is non-null and always required. Recognising that there is no in-between is most of the answer at this level.

for a middle

Explain why non-null is the wrong lever on an existing filter field — requiring what used to be optional breaks every document that omitted it, at validation time — and name the alternatives.

for a senior

Show the design move: publish the access paths you can serve as separate fields with real non-null anchors, rather than one hopeful field plus a hidden runtime rule. Be able to argue when the runtime check is nevertheless the right trade.

for a principal

Own the framing question — is the unfiltered call illegitimate or merely expensive — and the rollout: new fields rather than tightened old ones, evidence of what existing documents send, and a deprecation window you can actually observe.

## The incident this question comes from A farm sensor graph exposes `sensorReadings(filter: SensorReadingFilter, orderBy: SensorReadingOrder)`. Every field on the filter is nullable, because that is what optional means, so `sensorReadings(orderBy: { field: RECORDED_AT })` with no filter at all is a valid document. It matches every reading ever recorded, and the service ends up sorting an 8,400-row result per request to return the first page of it, several times a minute, from a dashboard nobody remembers building. The obvious fix was shipped in a deploy: tighten `SensorReadingFilter.stationId` from `ID` to `ID!`, so a caller must always name a station. Every saved dashboard document that filtered only by a date range stopped validating the moment the schema rolled out — on an input field, requiring something that used to be optional is the breaking direction, and it breaks at validation time for everyone at once, not gradually. ## What the type system can and cannot express It can express **always required**: a non-null argument or a non-null input field with no default. That is an unconditional obligation on every caller, which is exactly what broke the dashboards. It cannot express **at least one of these**. There is no modifier for it, no directive in the specification for it, and no way to say "either `stationId` or a bounded `recordedAfter`/`recordedBefore` pair". The nearest thing in the ecosystem is the one-of input pattern, which is a specification *draft* and expresses **exactly one** field — mutual exclusivity, not a minimum — so it does not solve this even when it is available. So the question is not "which modifier do I use". It is "which of four real options do I pick, knowing the schema cannot state the rule". ## Option 1 — model the access paths as separate fields Instead of one general field with a hopeful filter, publish the queries you can actually serve: ```graphql type Query { readingsByStation(stationId: ID!, recordedAfter: DateTime, recordedBefore: DateTime): [SensorReading!]! readingsByMetricWindow(metric: MetricKind!, recordedAfter: DateTime!, recordedBefore: DateTime!): [SensorReading!]! } ``` Now the type system does enforce it, because each field's anchor is genuinely non-null on every call. The cost is surface area: two fields where there was one, and a third when a new access path appears. This is the honest option, and on a graph with two or three real access patterns it is usually right. ## Option 2 — require an anchor argument on the one field Keep `sensorReadings` and lift the anchor out of the filter input into a required argument of its own: `sensorReadings(stationId: ID!, filter: SensorReadingFilter, orderBy: SensorReadingOrder)`. This works only when every legitimate caller really does have a station in hand. Introduce it on a **new** field, or from day one; converting the existing field is the same breaking change that started the incident. ## Option 3 — validate at execution and say so Leave the filter fields nullable and have the field reject an under-specified call with a clear, coded error and a message that names the acceptable combinations. This is the pragmatic fallback and it is often what ships. Be honest about its cost in an interview: the rule is now invisible to introspection, a typed client generator cannot enforce it, and callers only learn about it at runtime. Anything enforced here belongs in the field's description as well, because the description is the only place a reader can find it. ## Option 4 — bound the damage instead of requiring the predicate Sometimes the real requirement is not "filter something" but "do not let one call cost this much". Page-size limits and static cost scoring attack that directly and are separate mechanisms with their own designs; they are worth naming as the alternative framing, because a required predicate is a poor proxy for a cost bound — a caller can supply `stationId` for the busiest station and still be expensive. ## How to choose Ask whether the under-specified call is *illegitimate* or merely *expensive*. If it is illegitimate — nobody should ever ask for all readings — the schema should say so structurally, which means options 1 or 2, introduced as new fields rather than tightened onto old ones. If it is legitimate but costly, do not lie about it in the type system: allow it and bound it. And whichever you choose, do not fix it by tightening nullability on a filter field that clients are already omitting. That is a validation-time break for every existing document, delivered by a deploy, with no gradual signal beforehand.

  • Does the one-of input pattern solve this?
    No. It is a specification draft rather than a settled rule, and it expresses **exactly one** field supplied — mutual exclusivity — not a minimum. It is the right tool when a filter must be *either* a station *or* a serial number and never both. It cannot say "a station, or a bounded time window, or both", which is the usual shape of a selectivity requirement.
  • Why is a runtime check the pragmatic answer even though it is invisible to introspection?
    Because it costs nothing to ship and covers combinations the type system cannot describe. The price is that the contract has left the schema: a typed client generator cannot enforce it, a reader has to find it in the field's description, and callers learn about it at runtime. Ship it with a stable error code and a message naming the acceptable combinations, and treat the description as part of the contract.
  • Is requiring a filter the right way to bound the cost of a list field at all?
    Often not. A required `stationId` still lets a caller pick the busiest station over an unbounded time range. If the real goal is cost, attack it directly with page-size limits and static cost scoring, which are separate mechanisms designed for it. Require a predicate when the unfiltered call is illegitimate, not merely expensive.

A non-null field is a locked door; what you wanted was a turnstile that admits anyone holding any one of three tickets. The type system only ships doors.

saying these in an interview costs you the question

  • Claims a directive in the spec expresses at-least-one-of
  • Tightens an existing nullable filter field to non-null
  • Confuses exactly-one exclusivity with a minimum requirement
  • Thinks a runtime check is visible through introspection
  • Uses a required filter as a substitute for a cost limit
  • Says a breaking input change rolls out gradually

context