skip to content

What do the @skip and @include directives do in a GraphQL document?

level: juniorimportance: must knowfreq 63%

answer

  1. Only two, and the client writes them
  2. One boolean argument each, required
  3. Three places: field, spread, inline fragment
  4. Absent from the response, not null
  5. @include keeps on true, @skip drops on true

basics

~20 s

They let the client decide at request time whether part of its own selection is sent. @include(if:) keeps a selection only when its boolean is true; @skip(if:) drops it when true. An excluded selection is simply absent from the response.

solid answer

~50 s

`@skip` and `@include` are the two **executable directives** every GraphQL server must support — executable meaning the client writes them inside the document it sends, not in the schema. Each takes one required argument, `if`, of type `Boolean!`, supplied as a literal or, in practice, as an operation variable. `@include(if: $x)` keeps the selection when `$x` is true; `@skip(if: $x)` removes it when `$x` is true. They may be attached in exactly three places: a field, a fragment spread, or an inline fragment. Applying one to a fragment spread drops or keeps that fragment's whole slice. The effect on the payload is **absence, not null** — an excluded field has no response key at all — and the server never resolves it. That is what lets one document serve a collapsed and an expanded view of the same screen.

code

graphql · 19 lines
graphql
query EventPage($eventId: ID!, $expanded: Boolean!, $isVenueStaff: Boolean!) {
  event(id: $eventId) {
    title
    startsAt
    seatMap @include(if: $expanded) {
      sectionCount
    }
    ...SeatingDetail @include(if: $expanded)
    ... @include(if: $isVenueStaff) {
      grossSalesCents
      compedTicketCount
    }
  }
}

fragment SeatingDetail on Event {
  accessibleSeatCount
  restrictedViewSeatCount
}

go deeper

for a junior

Memorise the two names, the single required if: Boolean! argument, and the direction of each: @include keeps on true, @skip drops on true. Be ready to say that an excluded field is absent from the response, not null.

for a middle

Explain the three legal locations and why a spread or an inline fragment is often the better attachment point than repeating the directive on five sibling fields. Know that the argument position is non-null and what that means for variable declarations.

for a senior

Show why a single parameterised document beats runtime string concatenation — static validation, build-time codegen, and document registration all need fixed text. Be able to state plainly that these directives are not authorization.

for a principal

Own the house rule on how much conditionality one document may carry before it should be split into named operations, since each flag doubles the response shapes that clients, generated types and caches must handle.

## Two families of directive, and these are in the client's half GraphQL directives split into two families. **Type-system directives** are written in SDL, on the schema's own definitions — they annotate the API for whoever reads or builds it. **Executable directives** are written by the *client*, inside the executable document it sends, and they change how that one request behaves. `@skip` and `@include` are the only two executable directives the GraphQL specification requires every server to support, which is why they are the only conditional construct you can rely on against any server in any language. Their declarations, which every conforming schema carries implicitly: ```graphql directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT ``` ## The three legal locations, and the one people expect that is missing The location list above is exhaustive. You may attach either directive to: * a **field** in a selection set — `seatMap @include(if: $expanded)`; * a **fragment spread** — `...SeatingDetail @include(if: $expanded)`, which keeps or drops everything that fragment selects; * an **inline fragment** — `... on ReservedSeating @skip(if: $compact) { }`, and also the type-condition-less form `... @include(if: $expanded) { }`, which is the idiomatic way to guard a *group* of sibling fields without inventing a named fragment for them. They may **not** be attached to a fragment *definition*, to an operation definition, to a variable definition, or to an argument. That last exclusion is the one that surprises people: there is no way to conditionally supply an argument. You can drop a whole field, but you cannot say "send `first: 50` on weekdays and `first: 10` otherwise" from inside the document. ## The `if` argument One argument, named `if`, of type `Boolean!`, and it is required — `@skip` with no arguments is a validation error, not a no-op. The value may be a literal `true`/`false`, or an operation variable. Because the argument position is non-null, the variable is normally declared `Boolean!`. A nullable variable is also accepted when it carries a non-null default (`$expanded: Boolean = false`), because the specification's variable-usage rule allows a nullable variable in a non-null position if it has a non-null default value. ## Absence, not null This is the part worth being precise about, because the whole downstream story depends on it. A selection excluded by `@skip`/`@include` does not appear in the response object at all — there is no key holding `null`, and no entry in `errors`. Reading it back is the difference between "the key is missing" and "the value is null", and those are genuinely different states: `null` means the server resolved the field and it had no value; missing means the client never asked. The server also never invokes that field's resolver, so a guarded expensive field really is free when the flag is off. ## A ticketing example One document, two views of the same event page: ```graphql query EventPage($eventId: ID!, $expanded: Boolean!, $isVenueStaff: Boolean!) { event(id: $eventId) { title startsAt ...SeatingDetail @include(if: $expanded) ... @include(if: $isVenueStaff) { grossSalesCents compedTicketCount } } } fragment SeatingDetail on Event { seatMap { sectionCount accessibleSeatCount } } ``` With `expanded: false` and `isVenueStaff: false` the response object under `event` has exactly two keys, `title` and `startsAt`. Flip both variables and it has five. Same document, same server, no server-side branching. ## Why this exists at all Without these directives, a client that wants two shapes of one screen has two bad options: ship two nearly-identical documents and keep them in sync by hand, or build the query text by string concatenation at runtime — which defeats static validation, defeats build-time code generation, and defeats any scheme that registers documents ahead of time. `@skip`/`@include` keep the document a single, fixed, statically-checkable artefact whose *shape* is still parameterised. That is the whole design intent: the variables carry the branching, the text stays constant. ## What they are not They are not an authorization mechanism. The `if` value is supplied by the caller, so `grossSalesCents @include(if: $isVenueStaff)` hides the field from a well-behaved client and from nobody else — a caller who sets the variable to `true` gets whatever the field's own authorization allows. They are also not a general branching construct; the conditions are plain booleans the client already knows before it sends the request, and they can neither depend on data in the same response nor choose between two different field arguments.

  • If a client omits an excluded field's key entirely, how should the calling code tell that apart from a null value?
    By checking for key presence rather than for null. They mean different things: a missing key says the client never asked, a `null` value says the server resolved the field and it had no value — or that an error nulled it. Code that collapses the two loses the distinction, which matters most when a nullable field is guarded by a flag: `null` is data, absent is not.
  • Why is `@include(if: $isVenueStaff)` not a substitute for field-level authorization?
    Because the variable is supplied by the caller. The directive expresses what *this* client wants in the payload, not what the caller is permitted to see, and any caller can send `true`. Authorization has to be enforced where the field is resolved, so that an unauthorised caller who asks for the field is refused regardless of what the document's directives say.
  • Can either directive be attached to a named fragment's definition rather than its spread?
    No. The legal locations are field, fragment spread and inline fragment only, so the condition goes on the `...SeatingDetail` spread, not on `fragment SeatingDetail on Event`. That is deliberate: one fragment definition can be spread in several places, and each spread may want a different condition, so the condition belongs to the use site.

Think of the document as a printed order form with optional sections. The variables are the checkboxes: an unchecked section is not filled in blank, it is torn off before the form is handed over.

saying these in an interview costs you the question

  • Says a skipped field comes back as null
  • Thinks @skip and @include are written in the SDL schema
  • Believes they can be applied to a fragment definition
  • Thinks the if argument is optional or accepts a string
  • Uses @include as an access-control mechanism
  • Claims they can conditionally change a field's argument

context