skip to content

Type System & SDL

The Schema Definition Language and the type system it declares: scalars, objects, interfaces, unions, inputs, nullability modifiers. Interviewers start here because it is the contract others inherit.

part ofGraphQLoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

In GraphQL, what does applying a custom directive to a schema field actually do at execution time?

level: juniorimportance: must knowfreq 47%

answer

  1. Who defines what it means?
  2. Validation is not behaviour
  3. Undefined fails loudly, unread fails silently
  4. The server has to go looking
  5. Only the built-ins have specified semantics

basics

~10 s

Nothing by itself. GraphQL defines no execution semantics for custom directives, so an application is validated metadata the server must be programmed to find and act on. A directive nothing reads is documentation.

solid answer

~50 s

The GraphQL specification defines behaviour only for its own built-in directives — `@skip` and `@include` in documents, `@deprecated` and `@specifiedBy` in the schema. For any directive you declare yourself, the specification says what a valid application looks like and nothing about what it does. Validation checks three things: the directive is defined, the location is one its definition allows, and the argument values typecheck. A schema that passes those checks executes exactly as if the annotation were deleted, unless server code looks for it. That gives an important asymmetry: applying an **undefined** directive fails the schema build loudly, while applying a **defined but unread** one is completely silent. So the meaning lives in your code — a build-time pass that rewrites the schema, a hook that reads the annotation while a field resolves, or a build tool such as a linter or client generator that consumes it before runtime.

code

graphql · 9 lines
graphql
directive @unit(symbol: String!) on FIELD_DEFINITION

type Animal {
  id: ID!
  earTag: String!
  birthWeight: Float! @unit(symbol: "kg")
  sire: Animal
  dam: Animal
}

go deeper

for a junior

Recall the one-line rule: applying a custom directive changes nothing until server code reads it. Be able to name the built-ins the specification does define, and to say that a valid application is only validated metadata.

for a middle

Explain what validation actually checks — defined, allowed location, well-typed arguments — and contrast the loud failure of an undefined directive with the silent nothing of an unread one. Name who in a stack does the reading.

for a senior

Be ready to say why this matters operationally: annotation and behaviour are coupled only by a string, nothing warns you when they drift, and clients cannot observe applications, so a directive is never a client contract.

for a principal

Own the policy question: when a schema should express something as a directive at all versus in the types, and how a team keeps track of which consumer reads each directive as the schema and the tooling around it grow.

### Two families of directive, and the very short list with defined meaning GraphQL has two kinds of directive. *Executable* directives appear inside an operation document, attached to a selection, a fragment or a variable definition. *Type-system* directives appear in the SDL, attached to a schema element — a field definition, an argument definition, an object type, an enum value. A **custom** directive is simply one your own schema declares, as opposed to the handful the specification itself declares. The specification assigns behaviour to its own built-ins only. `@skip` and `@include` decide whether a selection is included in the result. `@deprecated` marks a field, argument or enum value as deprecated and is surfaced through introspection. `@specifiedBy` points a custom scalar at a human-readable specification URL. That list is the entire set of directives whose meaning GraphQL defines. For every directive you invent, the specification is deliberately silent: it defines the syntax for declaring one and for applying one, and stops there. ### What validation actually guarantees When you apply a custom directive, three things are checked: the directive must be defined in the schema, the location must be one its definition allows, and the supplied arguments must be valid for their declared types. Passing those checks means the schema is *valid*. It does not mean anything will happen. Take a livestock pedigree graph whose `Animal.birthWeight` field carries `@unit(symbol: "kg")`. That schema executes byte-for-byte identically to the same schema with the annotation deleted, unless somebody wrote server code that looks for it. The number that comes back is whatever the resolver returned; the directive did not convert it, validate it, or label it. This creates an asymmetry that catches candidates out. Applying an *undefined* directive is a hard failure — the schema does not build. Applying a *defined but unread* directive is completely silent. So the characteristic failure of a custom directive is never a loud error. It is the annotation quietly being decoration, in a schema that reviews well and passes every check. ### Where the meaning actually lives The meaning lives in code you write, and it is attached in one of three places: * **In the server, at schema-build time.** The builder walks the schema, finds the applied directive at each location, and rewrites what it found — most commonly by replacing the field's resolver with a wrapper that adds the behaviour. * **In the server, at execution time.** A resolver, or a generic hook running around every field, reads the applied directive from the field's schema definition while the field is being resolved and branches on it. * **Outside the server entirely.** Plenty of useful directives never reach runtime: a schema linter enforces a convention, a typed client generator emits different code for an annotated field, a build step strips the directive after consuming it. Nothing is wrong with a directive that is purely a build-time or documentation marker — as long as everyone knows that is what it is. Apollo Federation is the instructive counter-example. Its directives do have defined meanings — but they are defined by *that* composition specification, and honoured because composition tools implement it. The meaning still does not come from GraphQL; it comes from a second specification layered on top. ### The consequences to be able to state out loud 1. **Not portable.** SDL carrying custom directives can move between servers; the behaviour cannot, because the behaviour was never in the SDL. 2. **Not a client contract.** Aside from the two built-ins that introspection exposes, a client cannot see that a directive was applied, so it cannot depend on it. Anything a client must react to belongs in the types, not in an annotation. 3. **Partial application is normal and invisible.** If the handler covers field definitions but the directive is also applied to an argument definition, half the applications simply do nothing — and no tool tells you. 4. **A directive is a name-based coupling.** SDL text and server code agree by string. Rename one side and the schema still validates while the behaviour disappears. ### The interview shape The question is usually asked because a candidate has used a schema where `@auth`-style or `@constraint`-style directives appeared to enforce themselves. The strong answer separates the two layers immediately: the specification gives you a *validated, structured place to hang metadata on schema elements*, and your server supplies every bit of behaviour. Say that plainly, then name who in your stack does the reading, and the question is answered.

  • If a custom directive does nothing on its own, why put one in a schema at all?
    Because it is a validated, structured place to attach metadata to a schema element, and something in your pipeline can read it: a server pass that wraps resolvers, a schema linter enforcing a convention, a typed client generator, or a composition step. A directive that is purely documentation is also legitimate — the mistake is not knowing which consumer, if any, reads a given one.
  • Can a client see that a custom directive was applied to a field it queries?
    No. Aside from the built-ins the specification exposes, applications of custom directives are not surfaced to clients, so a client cannot detect or depend on one. Anything a client must react to belongs in the types themselves — a field, an argument, a nullability change — not in an annotation the client cannot observe.
  • What happens if you apply a directive that is not declared in the schema?
    The schema is invalid and the build fails. That is the asymmetry worth stating: an undefined directive is a hard error, while a perfectly defined directive that no code ever reads produces no error at all. The silent case is the one that reaches production.

An applied custom directive is a sticky note on a blueprint: perfectly legible and perfectly valid, but the building only changes if someone reads it and picks up a tool.

saying these in an interview costs you the question

  • Claims the directive enforces itself once applied
  • Thinks GraphQL defines semantics for any declared directive
  • Says a client can read applied custom directives
  • Expects an error when nothing consumes the directive
  • Believes SDL carries the behaviour to another server
  • Confuses @skip and @include with custom schema directives

context

open as a page

What does a directive definition in GraphQL SDL declare?

level: juniorimportance: must knowfreq 48%

basics

~20 s

A directive definition declares the directive's name, the arguments it accepts with their types and defaults, whether it may be applied more than once in the same place, and, mandatorily, the list of locations where it may be applied. It declares no behaviour.

open as a page

In GraphQL, why is an enum value unquoted in a query document but a quoted string in the JSON response?

level: juniorimportance: must knowfreq 55%

basics

~20 s

GraphQL's own grammar has an enum-value token, so a value like STORED is written as a bare name in a document. JSON has no enum token, so the same value crosses the wire as a string holding that name.

open as a page

In GraphQL SDL, what is the difference between an interface type and a union type?

level: juniorimportance: must knowfreq 74%

basics

~20 s

An interface declares fields that every implementing object type must declare too, so a client can select those shared fields on the abstract type itself. A union declares no fields at all — it only lists which object types the value may be.

open as a page

What is the __typename meta-field in GraphQL, and where can a client select it?

level: juniorimportance: must knowfreq 66%

basics

~20 s

__typename is a built-in meta-field selectable in any object, interface or union selection set. It returns the name of the concrete object type the server resolved, as a non-null String, so a client can tell which type it received.

open as a page

In GraphQL SDL, what does the `!` in a field type like `String!` mean, and what does a bare `String` mean?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Every GraphQL type is nullable by default, so a bare String means a string or null. The ! wraps the type in Non-Null: the field is promised to carry a value and can never be null.

open as a page

What is an object type in GraphQL SDL, and which of its fields may declare arguments?

level: juniorimportance: must knowfreq 74%

basics

~20 s

An object type is a named type holding a list of fields. Arguments belong to a field rather than to the type, so any field at any depth may declare them - not just the fields on the query root.

open as a page

What are GraphQL's five built-in scalar types, and how is ID different?

level: juniorimportance: must knowfreq 71%

basics

~20 s

Int, Float, String, Boolean and ID. ID marks a unique identifier: it accepts either a string or an integer as input, but is always serialized back as a string, and clients should treat it as opaque.

open as a page

What does the `schema` definition in GraphQL SDL declare, and what happens if you omit it?

level: juniorimportance: must knowfreq 63%

basics

~20 s

A schema definition names the object types that serve as entry points for the three operation kinds: query, mutation and subscription. Omit it and the roots default to types literally named Query, Mutation and Subscription.

open as a page

What is the difference between schema-first and code-first GraphQL schema construction?

level: juniorimportance: must knowfreq 66%

basics

~20 s

Schema-first means a hand-written SDL document is the source of truth and the server binds code to it. Code-first means the schema is built from server-side type declarations and printed to SDL afterwards, if at all.

open as a page

What makes a GraphQL schema invalid, and when is that caught?

level: juniorimportance: must knowfreq 52%

basics

~20 s

A schema is invalid when its own definitions break the type-system rules: a missing interface field, a union member that is not an object type, an argument typed as an output type. Servers check this once, while building the schema.

open as a page

Why does GraphQL split types into input and output kinds, and which kinds sit on both sides?

level: middleimportance: must knowfreq 62%

basics

~20 s

A caller supplies complete values while a server returns things a client selects fields from, so the two directions need different type kinds. Scalars and enums work in both directions; object, interface and union types are output-only, and input object types are input-only.

open as a page

What do GraphQL's __schema and __type(name:) meta-fields return, and where may they appear?

level: middleimportance: must knowfreq 52%

basics

~20 s

__schema returns the whole type system: every type, the query, mutation and subscription root types, and the directive definitions. __type(name:) returns one named type, or null when the schema has none. Both are legal only at a query operation's root.

open as a page

In GraphQL SDL, how do `[Skill!]!`, `[Skill]!`, `[Skill!]` and `[Skill]` differ?

level: middleimportance: must knowfreq 66%

basics

~20 s

Each ! governs one position, read inside out: the inner mark forbids null elements, the outer forbids a null list. So [Skill!]! allows neither null, [Skill]! allows null elements, [Skill!] a null list, and [Skill] both.

open as a page

How does a default value on a GraphQL field argument differ from making that argument Non-Null?

level: middleimportance: must knowfreq 61%

basics

~20 s

A default value decides what the server sees when the caller omits the argument. Non-Null decides whether null is an accepted value at all. They are independent: an argument can have a default, be Non-Null, both, or neither.

open as a page

What must a custom GraphQL scalar define beyond its name in the schema?

level: middleimportance: must knowfreq 58%

basics

~10 s

Two directions of coercion: result coercion, turning the server's internal value into the response value, and input coercion, reading a caller's value — from a literal in the document and from the variables map.

open as a page

How does a GraphQL server give behaviour to a custom directive applied in its schema?

level: middleimportance: should knowfreq 38%

basics

~20 s

Two shapes. Either a build-time pass walks the schema, finds each application and rewrites what it found — usually wrapping the field's resolver — or the code running a field reads the annotation off that field's schema definition at execution time and branches on it.

open as a page

In GraphQL, how do a directive's executable locations differ from its type-system locations?

level: middleimportance: should knowfreq 40%

basics

~20 s

Executable locations are positions in the operation document a client sends - a field selection, a fragment spread, an operation. Type-system locations are positions in the schema - an object type, a field definition, an enum value. A definition may list either family, or both.

open as a page

What must an object type declare to implement a GraphQL interface, and what may it change?

level: middleimportance: should knowfreq 46%

basics

~20 s

It must redeclare every interface field, with every argument the interface declared and the identical argument types. It may return a stricter type — a non-null or a subtype — add nullable arguments, and add fields of its own.

open as a page

What does `extend type` do in GraphQL SDL, and what may an extension not do?

level: middleimportance: should knowfreq 47%

basics

~20 s

It adds fields, interfaces or directives to a type already defined elsewhere in the same type system, without touching the original definition. Extensions may only add: redeclaring an existing field is a validation error, never an override.

open as a page

In schema-first GraphQL, how do the SDL file and the resolver code drift apart?

level: middleimportance: should knowfreq 50%

basics

~20 s

They are two artefacts nothing checks jointly. SDL can declare a field no code implements, code can implement a field the SDL never declares, and either side can be edited without the other. Most drift surfaces only at runtime.

open as a page

What must an object type do to validly implement an interface?

level: middleimportance: should knowfreq 54%

basics

~20 s

Declare every field the interface declares, returning the same type or a narrower subtype, and repeat every interface argument at exactly the same type. Extra arguments must not be required, and transitively implemented interfaces must also be listed.

open as a page

A custom GraphQL directive is applied in the SDL but has no effect in production — how do you find out why?

level: seniorimportance: should knowfreq 31%

basics

~20 s

Assume nothing reads it, because an ignored directive is a valid schema and raises no error. Check whether any handler matches that name, whether it visits that location kind, and whether a later rebuild or re-parse discarded the transform.

open as a page

In a large GraphQL schema, how do you manage the input object types that shadow output object types?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Treat each input as the argument shape of one operation rather than a writable copy of an entity. Divergence from the output type is expected and should be deliberate; the failure mode is a shared entity-shaped input reused by every mutation.

open as a page

When would you model an abstract GraphQL field as an interface rather than a union, and what does each cost later?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Use an interface when the alternatives share fields a client should read without knowing which one it got; use a union when they share nothing but the field that returns them. The later cost is asymmetric: interfaces get expensive to widen, unions get risky to extend.

open as a page

In GraphQL introspection, why is __Type.name null for a field typed [Artwork!]!, and how do you read it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

That field's type is not one type but a chain of __Type records: NON_NULL wrapping LIST wrapping NON_NULL wrapping the object type. Only the innermost, named record carries a name, so a consumer walks ofType down to it.

open as a page

In a GraphQL schema, what may a client send for a nullable input field that a Non-Null one forbids?

level: seniorimportance: should knowfreq 46%

basics

~20 s

A nullable input field accepts three distinguishable moves: omit it, send an explicit null, or send a value. A Non-Null one accepts only the last — null is never valid there, and omission fails validation unless a default supplies the value.

open as a page

In GraphQL, what does an argument on a nested field apply to when its parent resolves to many objects?

level: seniorimportance: should knowfreq 38%

basics

~20 s

It applies once per parent object, independently. The argument is written once in the document, but the nested field is executed for every parent, so the same values are used again for each one - never once across the whole response.

open as a page

A GraphQL field typed Int fails only for the oldest vehicles in a fleet — why, and what do you change?

level: seniorimportance: should knowfreq 41%

basics

~20 s

Int is specified as a signed 32-bit value, so it tops out at 2,147,483,647. High-mileage vehicles have crossed that boundary and their values cannot be coerced. Fix it by changing the field's type, not by clamping the data.

open as a page

When a GraphQL schema is assembled from several SDL documents, what actually depends on their order?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Meaning does not: extensions only add, and a duplicate field is a validation error rather than an override, so any legal document set yields the same type system. Only the printed field order changes — so diff schemas semantically, not as text.

open as a page

showing 1–30 of 38