skip to content

In a GraphQL schema, why type a list field's sort argument as an enum rather than a String?

level: juniorimportance: must knowfreq 64%

answer

  1. Where does the bad value get caught
  2. The schema, not the resolver, decides
  3. Introspection can list the legal keys
  4. Rejected during validation or variable coercion
  5. Each enum value is a promise to serve

basics

~20 s

An enum makes the set of sortable keys part of the schema. An unknown value is rejected before execution, introspection publishes the legal values, and no resolver has to defend itself against an arbitrary column name in a string.

solid answer

~50 s

A `String` sort argument accepts anything, so the check moves into the resolver: an unknown key becomes a field error at best, and a silent fallback to some default order at worst. Typing it as an enum — `orderBy: SensorReadingOrder` where the order input holds a `SensorReadingSortField` enum and an `OrderDirection` enum — puts the allow-list in the type system. A document naming a value the enum does not define fails validation, and a variable carrying one fails coercion; both happen before any resolver runs and produce a request error naming the argument. The enum is also published through introspection, so a reader or a typed client generator can enumerate the legal keys without reading server code. The cost is real: every enum value is a promise you can serve that ordering, and adding one is a schema change.

code

graphql · 19 lines
graphql
enum SensorReadingSortField {
  RECORDED_AT
  TEMPERATURE_C
  STATION_NAME
}

enum OrderDirection {
  ASC
  DESC
}

input SensorReadingOrder {
  field: SensorReadingSortField!
  direction: OrderDirection! = DESC
}

type Query {
  sensorReadings(orderBy: SensorReadingOrder): [SensorReading!]!
}

go deeper

for a junior

Be ready to write both shapes and to say which one catches a bad sort key before execution. Knowing that the enum values show up in introspection is the other half of the expected answer.

for a middle

Explain the mechanics precisely: a bad literal fails validation, a bad variable fails coercion, both produce a request error with no data, and the resolver never runs. Then contrast with the resolver-side check a String forces.

for a senior

Show that you treat each enum value as an operational commitment — an ordering you can serve at any page depth — and be able to argue for pair-style values when only some field-and-direction combinations are backed by an index.

for a principal

Own the policy: who is allowed to add a sort key, what evidence of index coverage is required first, and how keys are retired across many list fields without breaking documents you cannot see.

## The two shapes A list field has to be told what order to return things in. There are two ways to declare that on a farm sensor graph, and they differ in where the mistake is caught. ```graphql # free-form: the argument is a String type Query { sensorReadings(sortBy: String, order: String): [SensorReading!]! } # enumerated: the argument is an enum, wrapped in an order input enum SensorReadingSortField { RECORDED_AT TEMPERATURE_C STATION_NAME } enum OrderDirection { ASC DESC } input SensorReadingOrder { field: SensorReadingSortField! direction: OrderDirection! = DESC } type Query { sensorReadings(orderBy: SensorReadingOrder): [SensorReading!]! } ``` Both compile. Both are legal GraphQL. The second one moves the set of legal sort keys out of the resolver and into the type system, and everything else follows from that. ## Where the bad value is caught With the enum, a document that writes `orderBy: { field: SOIL_PH }` fails **validation**: the literal is not a defined value of `SensorReadingSortField`, so the whole request is rejected before a single resolver runs, and the response carries a request error with no `data`. If the value arrives in a variable instead, it is rejected during variable coercion, which also happens before execution. Either way the client gets a precise message naming the argument and the type, and the server has done no work. With the `String`, nothing is wrong as far as GraphQL is concerned. `"soil_ph"` is a perfectly good `String`. The failure moves into the resolver, where it becomes a field error at best and a silent wrong answer at worst — many hand-written resolvers fall back to a default order when the string does not match, so the client gets 8,400 readings back in an order it did not ask for and no indication that anything was ignored. ## The enum is the allow-list, and it is published The set of enum values *is* the answer to "what may this list be ordered by?", and it is introspectable. Anyone pointed at the schema — a person reading it, a typed client generator, a schema linter — can enumerate the legal sort keys without reading server code or documentation. A `String` argument publishes nothing; the real allow-list, if there is one, lives in a resolver where no client can see it, and clients discover it by trial and error against production. This is also what makes the argument safe. A string that names a column and is threaded into the storage layer is an injection surface and an accidental-scan surface at once. An enum value is a closed set the server maps to a concrete access path it has already decided it can serve. ## Direction as its own enum, or baked into the value Two shapes are in common use. Separating `field` and `direction`, as above, keeps the enums small: N fields plus 2 directions. Baking the pair into one value — `RECORDED_AT_DESC`, `TEMPERATURE_C_ASC` — doubles the enum but makes each *combination* explicit, which matters when you can serve descending time order cheaply and ascending order over a huge range badly. Only enumerate combinations you are willing to serve at any page depth. Multi-field sort is usually expressed by making the argument a list, `orderBy: [SensorReadingOrder!]`, with list position meaning precedence. That reading of list order is a convention the schema must state in the field's description; the specification says nothing about it. ## What an enum value commits you to Every value is a promise. Adding `TEMPERATURE_C` says you can order the whole filtered set by temperature, not just the rows that happen to be in memory. Removing a value later is a breaking change for any document that names it, which is why the enum is also the natural unit of deprecation: an enum value can carry `@deprecated(reason:)` and be hidden from ordinary introspection while existing documents keep working. One thing the enum does *not* give you: a guarantee that the order is total. Ties on a non-unique key are a real problem for cursor-based paging, and they are a separate design concern from which keys are legal. ## What to say in an interview Say that the enum moves the check from execution time to validation time, and that the same move publishes the allow-list through introspection. Then name the cost, because the strong answer always does: an enum value is a commitment to serve that ordering, and the enum has to be edited and redeployed to add one, which is exactly the friction you want in front of a promise about index coverage.

  • How would you let a client sort by two keys at once, and what does the schema have to state?
    Make the argument a list of order inputs — `orderBy: [SensorReadingOrder!]` — and read list position as precedence: the first entry is the primary key, the next breaks its ties. That reading is a convention, not something the specification defines, so the field's description has to say it. Only accept combinations you can actually serve; a two-key order you cannot index is a promise you will break under load.
  • When would you bake the direction into the enum value, as `RECORDED_AT_DESC`, instead of separating it?
    When the supported unit is the combination rather than the key. If descending time order is cheap and ascending order over the full range is not, separate `field` and `direction` enums advertise a combination you cannot serve. One enum of explicit pairs doubles the value count but makes each supported ordering an individual, deprecable declaration.
  • What happens when you need to remove a sort key that clients are using?
    Removing an enum value is a breaking change: any document naming it stops validating. The additive path is to mark the value `@deprecated(reason:)`, which hides it from ordinary introspection while existing documents keep working, then remove it once usage evidence shows nobody sends it. That is why the enum is the useful unit here — a `String` argument gives you nothing to deprecate.

A String sort argument is a suggestion box; an enum is a menu. The menu is shorter on purpose — the kitchen has agreed it can cook every item on it.

saying these in an interview costs you the question

  • Thinks a String sort argument is validated by GraphQL
  • Cannot say when an invalid enum value is rejected
  • Believes enum values are quoted strings in a document
  • Passes a client-supplied sort string into the storage layer
  • Says adding an enum value is free with no serving cost
  • Assumes removing an enum value is a safe additive change

context