Custom Scalars
Creating Custom Scalar Types
Scalar types represent primitive leaf values in GraphQL. While GraphQL provides built-in scalars like String, Int, Float, Boolean, and ID, you can create custom scalars for specialized data types like DateTime, Email, URL, and JSON.
Built-in Scalars
GraphQL includes five default scalar types that form the foundation of your schema.
scalar String # UTF-8 character sequence
scalar Int # Signed 32-bit integer
scalar Float # Signed double-precision floating-point value
scalar Boolean # true or false
scalar ID # Unique identifier (serialized as String)
type User {
id: ID! # Built-in ID scalar
name: String! # Built-in String scalar
age: Int # Built-in Int scalar
rating: Float # Built-in Float scalar
isActive: Boolean # Built-in Boolean scalar
}
When to Create Custom Scalars
Custom scalars are useful when you need specific validation, serialization, or parsing logic for a data type that's used across your schema.
- DateTime: ISO 8601 date/time strings with timezone support
- Email: Validated email addresses
- URL: Validated URLs with protocol
- JSON: Arbitrary JSON objects
- PhoneNumber: Validated phone numbers
- Currency: Monetary values with precision
- UUID: Universally unique identifiers
Declaring Custom Scalars
Custom scalars are declared in your schema just like built-in types.
scalar DateTime
scalar Email
scalar URL
scalar JSON
scalar PhoneNumber
type User {
id: ID!
name: String!
email: Email! # Custom Email scalar
website: URL # Custom URL scalar
phoneNumber: PhoneNumber
createdAt: DateTime! # Custom DateTime scalar
metadata: JSON # Custom JSON scalar
}
type Post {
id: ID!
title: String!
publishedAt: DateTime
author: User!
}
Implementing DateTime Scalar
The DateTime scalar handles date/time parsing, validation, and serialization.
import { GraphQLScalarType, Kind } from 'graphql';
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO 8601 date-time string (e.g., 2024-01-15T10:30:00Z)',
// Serialize to client (DB -> Client)
serialize(value) {
if (value instanceof Date) {
return value.toISOString();
}
if (typeof value === 'string') {
return new Date(value).toISOString();
}
throw new Error('DateTime must be Date object or ISO string');
},
// Parse from client input (Client -> Server)
parseValue(value) {
if (typeof value === 'string') {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new Error('Invalid DateTime format');
}
return date;
}
throw new Error('DateTime must be a string');
},
// Parse from query literal
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
const date = new Date(ast.value);
if (isNaN(date.getTime())) {
throw new Error('Invalid DateTime format');
}
return date;
}
return null;
}
});
// Register resolver
const resolvers = {
DateTime: DateTimeScalar
};
- serialize: Converts internal value to client-facing format (response)
- parseValue: Parses variable input from client (variables in mutations)
- parseLiteral: Parses inline query literals (hardcoded values in queries)
Implementing Email Scalar
The Email scalar validates email addresses using regex patterns.
import { GraphQLScalarType, Kind } from 'graphql';
const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
const EmailScalar = new GraphQLScalarType({
name: 'Email',
description: 'Valid email address',
serialize(value) {
if (typeof value !== 'string') {
throw new Error('Email must be a string');
}
if (!EMAIL_REGEX.test(value)) {
throw new Error('Invalid email format');
}
return value.toLowerCase();
},
parseValue(value) {
if (typeof value !== 'string') {
throw new Error('Email must be a string');
}
if (!EMAIL_REGEX.test(value)) {
throw new Error('Invalid email format');
}
return value.toLowerCase();
},
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
if (!EMAIL_REGEX.test(ast.value)) {
throw new Error('Invalid email format');
}
return ast.value.toLowerCase();
}
return null;
}
});
const resolvers = {
Email: EmailScalar
};
Implementing URL Scalar
import { GraphQLScalarType, Kind } from 'graphql';
const URLScalar = new GraphQLScalarType({
name: 'URL',
description: 'Valid URL with protocol',
serialize(value) {
try {
const url = new URL(value);
return url.href;
} catch {
throw new Error('Invalid URL format');
}
},
parseValue(value) {
try {
const url = new URL(value);
// Ensure protocol is http or https
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('URL must use http or https protocol');
}
return url.href;
} catch {
throw new Error('Invalid URL format');
}
},
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
try {
const url = new URL(ast.value);
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('URL must use http or https protocol');
}
return url.href;
} catch {
throw new Error('Invalid URL format');
}
}
return null;
}
});
Implementing JSON Scalar
The JSON scalar allows arbitrary JSON objects as field values.
import { GraphQLScalarType, Kind } from 'graphql';
const JSONScalar = new GraphQLScalarType({
name: 'JSON',
description: 'Arbitrary JSON value',
serialize(value) {
return value; // Already a JS object
},
parseValue(value) {
return value; // Client sends JSON
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value;
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value);
case Kind.OBJECT:
return parseObject(ast);
case Kind.LIST:
return ast.values.map(n => JSONScalar.parseLiteral(n));
default:
return null;
}
}
});
function parseObject(ast) {
const value = Object.create(null);
ast.fields.forEach(field => {
value[field.name.value] = JSONScalar.parseLiteral(field.value);
});
return value;
}
Using graphql-scalars Library
Instead of implementing scalars from scratch, use the graphql-scalars library which provides production-ready implementations.
# Install
npm install graphql-scalars
# Usage
import {
DateTimeResolver,
EmailAddressResolver,
URLResolver,
JSONResolver,
PhoneNumberResolver,
UUIDResolver
} from 'graphql-scalars';
const resolvers = {
DateTime: DateTimeResolver,
Email: EmailAddressResolver,
URL: URLResolver,
JSON: JSONResolver,
PhoneNumber: PhoneNumberResolver,
UUID: UUIDResolver
};
# Common Scalars DateTime, Date, Time, Duration EmailAddress, PhoneNumber, URL UUID, GUID, HexColorCode, RGB, RGBA JSON, JSONObject BigInt, Long, Byte PositiveInt, NonNegativeInt, NegativeInt PositiveFloat, NonNegativeFloat, NegativeFloat # Advanced MAC, IPv4, IPv6 Port, Latitude, Longitude PostalCode, Currency SafeInt, Void
Validation Best Practices
import { GraphQLScalarType, Kind } from 'graphql';
const PhoneNumberScalar = new GraphQLScalarType({
name: 'PhoneNumber',
description: 'International phone number (E.164 format)',
serialize(value) {
return validateAndFormat(value);
},
parseValue(value) {
return validateAndFormat(value);
},
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
return validateAndFormat(ast.value);
}
return null;
}
});
function validateAndFormat(value) {
if (typeof value !== 'string') {
throw new Error('PhoneNumber must be a string');
}
// Remove whitespace and formatting
const cleaned = value.replace(/[\s()-]/g, '');
// E.164 format: +[country code][number]
const e164Regex = /^\+[1-9]\d{1,14}$/;
if (!e164Regex.test(cleaned)) {
throw new Error(
'PhoneNumber must be in E.164 format (e.g., +14155552671)'
);
}
return cleaned;
}
- Create a
HexColorscalar that validates hex color codes (#RGB or #RRGGBB) - Implement a
Currencyscalar that stores amounts with 2 decimal precision - Build a
Markdownscalar that validates Markdown syntax - Create a
LatitudeandLongitudescalar with range validation (-90 to 90, -180 to 180) - Use the
graphql-scalarslibrary to add DateTime, Email, and URL scalars to your schema