Custom Scalars: DateTime, JSON and Validation

Intermediate
11 min

Custom Scalars: DateTime, JSON and Validation

GraphQL ships with only five scalars: Int, Float, String, Boolean and ID. Real APIs also need timestamps, e-mail addresses and occasionally arbitrary JSON. After this lesson you will be able to declare a custom scalar in SDL, implement its three conversion functions, and reuse the ready-made scalars from the graphql-scalars package.

Why Not Just Use String?

A createdAt: String! field works, but it says nothing about the format, accepts any text as input, and forces every resolver to validate dates by hand. A custom scalar centralises that work:

graphql
scalar DateTime type Post { id: ID! title: String! createdAt: DateTime! } type Mutation { schedulePost(id: ID!, publishAt: DateTime!): Post! }

The schema now documents the format, invalid values are rejected before a resolver runs, and code generators can map DateTime to Date.

The Three Conversion Functions

A scalar is implemented with GraphQLScalarType from the graphql package. Three functions cover the three places where a value crosses a boundary:

| Function | Direction | Called when | |---|---|---| | serialize(value) | Server to client | A resolver returns the value and it must become JSON | | parseValue(value) | Client to server | The value arrives through a variable | | parseLiteral(ast) | Client to server | The value is written inline in the query document |

parseLiteral receives an AST node rather than a plain value because inline literals pass through the GraphQL parser first, so check ast.kind before using ast.value:

javascript
import { GraphQLScalarType, Kind, GraphQLError } from "graphql"; export const DateTime = new GraphQLScalarType({ name: "DateTime", description: "An ISO 8601 timestamp", serialize(value) { const date = value instanceof Date ? value : new Date(value); if (Number.isNaN(date.getTime())) throw new GraphQLError("Invalid date"); return date.toISOString(); }, parseValue(value) { const date = new Date(value); if (Number.isNaN(date.getTime())) throw new GraphQLError(`Invalid DateTime: ${value}`); return date; }, parseLiteral(ast) { if (ast.kind !== Kind.STRING) throw new GraphQLError("DateTime must be a string"); return new Date(ast.value); }, });

Register it in the resolver map under the scalar's name, next to your object types: const resolvers = { DateTime, Query: { ... } }. Resolvers now receive a real Date in args.publishAt and can return Date objects directly; the scalar converts them to ISO strings in the response. Throwing a GraphQLError from parseValue produces a validation error in the errors array and the operation never reaches the resolver.

Reusing graphql-scalars

You rarely need to write scalars yourself. The graphql-scalars package provides tested implementations of more than fifty common types, each exported as a type definition string and a resolver:

bash
npm install graphql-scalars
javascript
import { DateTimeTypeDefinition, DateTimeResolver, EmailAddressTypeDefinition, EmailAddressResolver, JSONTypeDefinition, JSONResolver, } from "graphql-scalars"; const typeDefs = [DateTimeTypeDefinition, EmailAddressTypeDefinition, JSONTypeDefinition, appTypeDefs]; const resolvers = { DateTime: DateTimeResolver, EmailAddress: EmailAddressResolver, JSON: JSONResolver, ...appResolvers };

Other useful members include Date, URL, UUID, PositiveInt, NonEmptyString and BigInt. The JSON scalar passes any value through untouched, which is convenient for settings blobs or webhook payloads whose shape you do not control. Use it sparingly: a JSON field loses field selection, validation and generated types, so model known structures as object types instead.

Common Mistakes

  • Returning a Date from serialize instead of a string; the result must be JSON-serialisable.
  • Forgetting parseLiteral, which makes inline literals fail while variables work.
  • Declaring scalar DateTime in SDL but omitting the resolver, which silently disables validation.
  • Using Int for millisecond timestamps; it is a signed 32-bit value and overflows.
Quick Quiz
Question 1 of 2

Which function runs when a client sends a custom scalar value through a variable?

Key Takeaways

  • Custom scalars add typed, validated leaf values such as DateTime and EmailAddress to a schema.
  • Implement serialize for output, parseValue for variables and parseLiteral for inline literals.
  • Throw GraphQLError from the parse functions to reject bad input before resolvers run.
  • Register the scalar in the resolver map under its SDL name.
  • graphql-scalars provides ready-made scalars; use JSON only for genuinely unstructured data.

Next lesson: Interfaces, Unions and Enums — model shared fields, heterogeneous results and fixed sets of values in your schema.

Custom Scalars: DateTime, JSON and Validation - GraphQL | CodeYourCraft | CodeYourCraft