skip to content

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

level: middleimportance: must knowfreq 58%

answer

  1. The SDL line declares a name, not behaviour
  2. Coercion runs in two directions
  3. The inbound direction has two entry points
  4. One path is syntax, the other decoded
  5. Introspection tells the client only the name

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.

solid answer

~50 s

In SDL a custom scalar is one line — `scalar GeoPoint` — which declares the name and nothing about behaviour. The behaviour is the contract the server must supply. **Result coercion** converts the value a resolver returned into something representable in the response; if it cannot, coercion fails rather than emitting a malformed value. **Input coercion** runs the other way, and it has two entry points that are easy to get half-right: a value written as a **literal inside the document**, which arrives as a parsed syntax node whose kind (string, int, list, object) the scalar must accept or reject, and a value supplied in the request's **variables map**, which arrives as an already-decoded value. A scalar handling one path but not the other works until a client switches an inline literal to a variable. It is also the only place a *format* can be enforced, since introspection reports the name and nothing more.

code

graphql · 10 lines
graphql
scalar GeoPoint

type Vehicle {
  id: ID!
  lastKnownPosition: GeoPoint
}

type Query {
  vehiclesNear(centre: GeoPoint!, radiusMetres: Int!): [Vehicle!]!
}

go deeper

for a junior

Know that scalar Foo declares a name only, and that the server has to say how values of that type are turned into a response value and read back out of a request.

for a middle

Name both directions and, in the inbound direction, both entry points — a literal in the document and a value in the variables map. That second detail is what separates a memorised answer from a real one.

for a senior

Talk about the coercion function as the schema's single validation boundary for that type, and about the downstream cost: an unmapped scalar becomes an untyped value in every generated client.

for a principal

Own the policy question of which custom scalars are allowed to exist at all, since each one is an ecosystem-wide client configuration burden and an unenforced agreement between teams about what the value means.

## The declaration says almost nothing ```graphql scalar GeoPoint ``` That is the entire SDL surface of a custom scalar. Unlike an object type, which enumerates its fields, or an enum, which enumerates its values, a scalar declaration is a *name and a promise*. Everything a caller would want to know — what shape the value has on the wire, what strings are acceptable, whether it is a number or text — lives outside the type system, in whatever the server implements and whatever documentation you attach. This is why the interview question is not "how do you declare one" but "what do you have to define". ## Two directions, three entry points A scalar is defined by coercion, and coercion runs both ways. **Result coercion** is the outbound direction. Whatever the resolver returned — a domain object, a date, a decimal, a pair of doubles — has to become a value that can appear in the response. This runs as the field's value is completed, and it is the last chance to reject nonsense: a scalar that quietly serializes an unrepresentable value ships corruption to clients, so a coercion that cannot succeed must fail instead. **Input coercion** is the inbound direction, and it has *two* entry points, because a caller can supply the value in two different ways. 1. As a **literal written inside the executable document**. This value arrives having been parsed as syntax, so what the scalar sees is a node with a kind: a string token, an int token, a float token, a list, an input-object shape. A `GeoPoint` scalar written to accept a string literal will simply reject `{ lat: 51.5, lon: -0.12 }` unless it was also written to accept an object node. 2. As a value in the request's **variables map**. That value has already been decoded from the request body, so the scalar receives a plain decoded value — a string, a number, a boolean, a list, a map — with no syntax node around it. The classic defect is a scalar that implements only one of the two. Everything passes review, because the tests use inline literals; then a client that parameterises the same argument as a variable starts failing, or vice versa. When you are asked this question, naming *both* input paths is the answer the interviewer is listening for. ## Coercion is where format is enforced A custom scalar is the only place the type system can say anything about *format*. The schema can say a field is a `GeoPoint`; it cannot say `GeoPoint` is `"lat,lon"` with six decimal places. So the coercion function is simultaneously a converter and a validator, and every rule you want enforced — the string parses, the range is sane, the precision is bounded — has to live there or nowhere. That gives custom scalars a genuinely useful property: validation happens *once*, at the boundary, for every argument and input field of that type across the whole schema, before any resolver runs. A fleet telematics graph that types every position argument as `GeoPoint` gets its coordinate validation in one place rather than in forty resolvers. ## What it costs on the other side of the wire The cost lands on clients, and it is worth being able to state it. Introspection reports a custom scalar as a scalar with a name — that, plus an optional specification URL, is the whole machine-readable story. So a typed client generator has no idea what `GeoPoint` is. Its options are to type it as an unknown/opaque value, or to be *configured* with a mapping from that scalar name to a client-side type. Every custom scalar you add is therefore a configuration entry in every client codebase that consumes the schema, and a scalar added without telling those teams shows up as an untyped hole in their generated code. There is a second, quieter cost: because the value is opaque to the type system, nothing stops two teams from meaning slightly different things by the same scalar name, and nothing catches it at composition time either. ## Two properties people forget * **A custom scalar is a leaf.** No selection set may be written beneath it, exactly like a built-in. If you want the client to pick parts of the value, you wanted an object type, not a scalar. * **A custom scalar may be used on both sides.** Scalars and enums are the two kinds of type valid as both output types and input types, so the same `GeoPoint` can be a field's type and an argument's type. Object types cannot do this — input positions need input object types. ## Spec versus convention The specification requires the coercions and requires that an impossible coercion raise an error rather than produce a value. It does *not* define any particular custom scalar: `DateTime`, `JSON`, `UUID`, `Long` and friends are conventions the ecosystem converged on, and two servers that both offer a `DateTime` are not guaranteed to agree on its serialization. How a server registers the coercion behaviour — which functions, which interface, which registration call — is entirely a matter of the server you are using, and no part of the specification.

  • Why are there two input coercion paths rather than one?
    Because the two sources arrive in different shapes. A literal is part of the document's syntax, so it reaches the scalar as a parsed node with a kind — a string token, an int token, a list, an input-object shape. A variable's value came from the request body and has already been decoded into a plain value. A scalar that only handles one shape fails the moment a client moves the value from inline to a variable.
  • What does a typed client generator do with a custom scalar it has never seen?
    Nothing useful by default. Introspection gives it a name and no structure, so the generated type is an opaque or unknown value unless the generator is explicitly configured to map that scalar name onto a client-side type. That configuration entry is the real, recurring cost of every custom scalar — it has to be added in every client codebase.
  • Can a client select subfields of a custom scalar to fetch only part of it?
    No. A scalar is a leaf type, so a selection set beneath it is invalid and the document is rejected. If callers need to choose parts of the value — say latitude without longitude — the value should be an object type with fields, not a scalar. Choosing a scalar is choosing all-or-nothing on the client's behalf.

A custom scalar is a customs desk on the schema's border: everything of that type is inspected once on the way in and stamped once on the way out, and travellers arrive both on foot and by train — two doors, one desk.

saying these in an interview costs you the question

  • Thinks `scalar Foo` in SDL defines behaviour
  • Names only serialization and forgets input coercion
  • Handles variables but not document literals
  • Says introspection tells clients the scalar's format
  • Tries to select subfields beneath a custom scalar
  • Assumes two servers' DateTime scalars serialize identically

context