skip to content

Built-in & Custom Scalars

The five built-in scalars, and what a custom one owes: result coercion out, input coercion in from a literal or a variable. Interviewers use it to see if a scalar reads as behaviour to you.

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

questions

4

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

level: juniorimportance: must knowfreq 71%

answer

  1. The response tree has to end somewhere
  2. Five names, one of them odd
  3. One scalar is asymmetric in and out
  4. Thirty-two bits, not sixty-four
  5. String or integer in, string always out

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.

solid answer

~50 s

The specification defines five built-in scalars. `Int` is a signed 32-bit non-fractional value; `Float` is a signed double-precision finite value; `String` is a UTF-8 character sequence; `Boolean` is true or false. `ID` is the odd one. It represents a unique identifier — the value a caller passes back to refetch an object or uses as a cache key — and it is asymmetric on purpose: as an **input** type it accepts either a string or an integer literal, so `vehicle(id: 4712)` and `vehicle(id: "4712")` are both legal; as an **output** type it is always serialized as a string, so a resolver returning the number 4712 puts `"4712"` in the response. The specification also says an ID is not intended to be human-readable, which is a hint to clients not to parse it. Note what is absent: there is no built-in date, decimal, UUID or JSON scalar.

code

graphql · 11 lines
graphql
type Vehicle {
  id: ID!
  registrationPlate: String!
  axleCount: Int!
  lastFuelLevelPercent: Float
  isInService: Boolean!
}

type Query {
  vehicle(id: ID!): Vehicle
}

go deeper

for a junior

Be able to list the five names without hesitating, and say the one sentence about ID: string or integer in, always string out. Knowing there is no built-in date scalar is the other half of the answer.

for a middle

Explain each built-in as a pair of coercions rather than as a JSON type name, and be ready for the 32-bit range of Int and the finiteness of Float — those two constraints are where real schemas break.

for a senior

Show that you treat ID as a contract with clients: opaque, non-parseable, serialized as a string. Expect to discuss what typing a numeric key as ID does to consumers already reading it as a number.

for a principal

Own the position that the missing built-ins are a schema-wide policy decision: every custom scalar you allow is a mapping every client generator in the organisation must be configured for, so which ones exist is worth deciding once, centrally.

## Scalars are the leaves of the response tree A GraphQL response is a tree, and every branch of that tree has to stop somewhere. It stops at a **leaf type** — a scalar or an enum. These are the only types that carry no selection set: you can write `vehicle { id }`, but you cannot write `id { something }`, because `ID` is a scalar and there is nothing beneath it. A scalar is more than a label pinned on a JSON value. In the type system a scalar is *defined by two coercions*: * **Result coercion** — turning whatever the server holds internally into the value that goes into the response. * **Input coercion** — turning a value a caller supplied, either as a literal written in the document or as a value in the request's variables map, into whatever the server wants to hand a resolver. The five built-ins are simply the five scalars whose coercions the specification writes down for you, so that every server and every client agree on them without negotiating. ## Int `Int` is a **signed 32-bit non-fractional value** — roughly -2.1 billion to +2.1 billion. This is a specified constraint, not an implementation detail, and it is the single most frequently discovered surprise in the list. A fleet telematics graph that types `Vehicle.odometerMetres` as `Int` is fine for years and then is not, because a vehicle's cumulative metres eventually crosses the boundary. `Int` is not "an integer"; it is a 32-bit integer. ## Float `Float` is a **signed double-precision finite value** as described by IEEE 754. "Finite" is doing real work in that sentence: `NaN` and `Infinity` are not representable, so a resolver that computes an average over an empty set of readings and returns `NaN` has produced something the scalar cannot serialize. Input coercion is deliberately forgiving in one direction: an integer literal is a valid `Float` input, so `speedLimitKph: 60` is accepted for a `Float` argument. The reverse is not true — `4.5` is not a valid `Int` input. ## String `String` is textual data represented as a UTF-8 character sequence. It is the default for free text, and also the type every schema reaches for when it has not yet decided whether the value is really an enum, a date or an identifier. ## Boolean `Boolean` is `true` or `false`. Worth saying out loud in an interview: **nullability is a separate axis**. A field typed `Boolean` has three possible response values — `true`, `false` and `null`. If you meant two, you wrote the type wrong; you wanted `Boolean!`. ## ID — the one interviewers actually ask about `ID` represents a unique identifier, typically the value used to refetch an object or as a cache key. Its behaviour is asymmetric, and that asymmetry is the whole question: * **Coming in**, an `ID` argument accepts a string *or* an integer. This exists because backends have numeric primary keys and clients tend to hold strings, and the specification refuses to make you pick. `vehicle(id: 4712)` and `vehicle(id: "4712")` are both valid. * **Going out**, an `ID` is serialized the same way a `String` is. A resolver that returns the integer `4712` for an `ID` field produces `"4712"` in the JSON, not `4712`. A client written against a numeric field will break the day someone types it as `ID`. The specification also states that an `ID` is *not intended to be human-readable*. That is a message to the client: treat the value as opaque, do not parse it, do not sort on it, do not assume it is numeric. It says nothing about uniqueness enforcement — the server guarantees nothing; `ID` is documentation of intent plus a serialization rule, not a constraint. ## What is deliberately missing There is no built-in date, time, decimal, URL, UUID or JSON scalar. Every schema you have seen with a `DateTime` field defined one itself; those names are **widespread convention, not specification**. The absence is intentional: the specification declines to pick a serialization for values whose format is genuinely contested, and leaves you to declare a custom scalar and document it. ## A worked shape A telematics `Vehicle` uses four of the five in one type: an `ID` for the vehicle, a `String` for its plate, an `Int` for a small count, a `Float` for a measured value, and a `Boolean!` for a flag that must never be null. Everything else — a timestamp, a coordinate, a large odometer count — is where a schema starts needing types the specification does not hand you.

  • A resolver for an ID field returns the integer 4712. What does the client receive?
    The string `"4712"`. Result coercion for `ID` serializes the same way `String` does, so the number becomes a JSON string. This is why a client that was reading a numeric field and starts reading an `ID` field breaks on a strict type check even though nothing about the underlying value changed.
  • Why is there no built-in DateTime scalar?
    Because there is no uncontested serialization for a date-time, and the specification declines to impose one. Schemas that expose timestamps declare a custom scalar and document its format — the name `DateTime` is a widespread convention, not something the specification defines. The cost is that every client has to be told what that scalar means.
  • Can a client write a selection set beneath a field typed Float?
    No. Scalars are leaf types: they have no fields, so a selection set on one is a validation error and the document is rejected before execution. Conversely, a field whose type is an object, interface or union *must* carry a selection set — both rules exist so every branch of the response tree terminates in a scalar or enum.

ID is like a coat-check ticket: the desk will take it written on paper or punched as a number, always hands it back as printed text, and expects you not to read anything into the digits.

saying these in an interview costs you the question

  • Says Int is a 64-bit integer
  • Thinks ID guarantees uniqueness in the server
  • Expects an ID field to serialize as a number
  • Names DateTime or JSON as a built-in scalar
  • Thinks a Boolean field has only two possible values
  • Believes Float can carry NaN or Infinity

context

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

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

What does GraphQL's @specifiedBy directive attach to a custom scalar, and who reads it?

level: juniorimportance: nice to knowfreq 18%

basics

~20 s

It attaches a URL to a custom scalar definition, pointing at a human-readable specification of how that scalar's values are serialized. The server never fetches or enforces it — humans and tooling read it through introspection.

open as a page