---
title: Best Practices for Custom Scalars
---
import { Callout } from 'nextra/components';
# Custom Scalars: Best Practices and Testing
Custom scalars must behave predictably and clearly. To maintain a consistent, reliable
schema, follow these best practices.
<Callout type="info">
GraphQL.js v17 renames the scalar hooks to the coercion terms used by the
specification. The v16 names still work in v17, but are deprecated for
removal in v18.
</Callout>
### Map v16 names to v17 names
| v16 name | v17 name | Purpose |
| --- | --- | --- |
| `serialize` | `coerceOutputValue` | Convert resolver values into response values. |
| `parseValue` | `coerceInputValue` | Convert variable values into internal values. |
| `parseLiteral` | `coerceInputLiteral` | Convert constant GraphQL literals into internal values. |
| `astFromValue()` | `valueToLiteral()` | Convert external input values into GraphQL literals. |
If your scalar supports both v16 and v17, keep the v16 method names for now.
When your minimum version is v17, implement the v17 names and add
`valueToLiteral()` if tooling needs to print defaults or external values for
that scalar.
In v16, `parseLiteral(ast, variables)` could receive variable values directly.
In v17, `coerceInputLiteral()` receives a constant literal. During execution,
GraphQL.js calls `replaceVariables()` before scalar literal coercion. If
tooling calls `coerceInputLiteral()` directly outside execution, it must call
`replaceVariables()` first.
### Document expected formats and validation
Provide a clear description of the scalar's accepted input and output formats. For example, a
`DateTime` scalar should explain that it expects [ISO-8601](https://www.iso.org/iso-8601-date-and-time-format.html) strings ending with `Z`.
Clear descriptions help clients understand valid input and reduce mistakes.
### Validate consistently across value and literal coercion
Clients can send values either through variables or inline literals.
Your value and literal coercion functions should apply the same validation
logic in both cases. In v16 those functions are `parseValue` and
`parseLiteral`; in v17 they are `coerceInputValue` and `coerceInputLiteral`.
Use a shared helper to avoid duplication:
```js
function parseDate(value) {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new TypeError(`DateTime cannot represent an invalid date: ${value}`);
}
return date;
}
```
Both input coercion paths should call this function.
### Return clear errors
When validation fails, throw descriptive errors. Avoid generic messages like "Invalid input."
Instead, use targeted messages that explain the problem, such as:
```text
DateTime cannot represent an invalid date: `abc123`
```
Clear error messages speed up debugging and make mistakes easier to fix.
### Serialize consistently
Always serialize internal values into a predictable format.
For example, a `DateTime` scalar should always produce an ISO string, even if its
internal value is a `Date` object.
```js
serialize(value) {
if (!(value instanceof Date)) {
throw new TypeError('DateTime can only serialize Date instances');
}
return value.toISOString();
}
```
Serialization consistency prevents surprises on the client side.
## Testing custom scalars
Testing ensures your custom scalars work reliably with both valid and invalid inputs.
Tests should cover three areas: coercion functions, schema integration, and error handling.
### Unit test serialization and parsing
Write unit tests for each function: `serialize`, `parseValue`, and
`parseLiteral` in v16, or `coerceOutputValue`, `coerceInputValue`, and
`coerceInputLiteral` in v17. Test with both valid and invalid inputs.
```js
describe('DateTime scalar', () => {
it('serializes Date instances to ISO strings', () => {
const date = new Date('2024-01-01T00:00:00Z');
expect(DateTime.serialize(date)).toBe('2024-01-01T00:00:00.000Z');
});
it('throws if serializing a non-Date value', () => {
expect(() => DateTime.serialize('not a date')).toThrow(TypeError);
});
it('parses ISO strings into Date instances', () => {
const result = DateTime.parseValue('2024-01-01T00:00:00Z');
expect(result).toBeInstanceOf(Date);
expect(result.toISOString()).toBe('2024-01-01T00:00:00.000Z');
});
it('throws if parsing an invalid date string', () => {
expect(() => DateTime.parseValue('invalid-date')).toThrow(TypeError);
});
});
```
### Test custom scalars in a schema
Integrate the scalar into a schema and run real GraphQL queries to validate end-to-end behavior.
```js
import { graphql, GraphQLSchema, GraphQLObjectType } from 'graphql';
import { DateTimeResolver as DateTime } from 'graphql-scalars';
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
now: {
type: DateTime,
resolve() {
return new Date();
},
},
},
});
/*
scalar DateTime
type Query {
now: DateTime
}
*/
const schema = new GraphQLSchema({
query: Query,
});
async function testQuery() {
const response = await graphql({
schema,
source: '{ now }',
});
console.log(response);
}
testQuery();
```
Schema-level tests verify that the scalar behaves correctly during execution, not just
in isolation.
## Common use cases for custom scalars
Custom scalars solve real-world needs by handling types that built-in scalars don't cover.
- `DateTime`: Serializes and parses ISO-8601 date-time strings.
- `Email`: Validates syntactically correct email addresses.
```js
function validateEmail(value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new TypeError(`Email cannot represent invalid email address: ${value}`);
}
return value;
}
```
- `URL`: Ensures well-formatted, absolute URLs.
```js
function validateURL(value) {
try {
new URL(value);
return value;
} catch {
throw new TypeError(`URL cannot represent an invalid URL: ${value}`);
}
}
```
- `JSON`: Represents arbitrary JSON structures, but use carefully because it bypasses
GraphQL's strict type checking.
## When to use existing libraries
Writing scalars is deceptively tricky. Validation edge cases can lead to subtle bugs if
not handled carefully.
Whenever possible, use trusted libraries like [`graphql-scalars`](https://www.npmjs.com/package/graphql-scalars). They offer production-ready
scalars for DateTime, EmailAddress, URL, UUID, and many others.
### Example: Handling email validation
Handling email validation correctly requires dealing with Unicode, quoted local parts, and
domain validation. Rather than writing your own regex, it's better to use a library scalar
that's already validated against standards.
If you need domain-specific behavior, you can wrap an existing scalar with custom rules:
```js
import { EmailAddressResolver } from 'graphql-scalars';
const StrictEmailAddress = new GraphQLScalarType({
...EmailAddressResolver,
name: 'StrictEmailAddress',
parseValue(value) {
const email = EmailAddressResolver.parseValue(value);
if (!email.endsWith('@example.com')) {
throw new TypeError('Only example.com emails are allowed.');
}
return email;
},
parseLiteral(literal, variables) {
const email = EmailAddressResolver.parseLiteral(literal, variables);
if (!email.endsWith('@example.com')) {
throw new TypeError('Only example.com emails are allowed.');
}
return email;
},
});
```
By following these best practices and using trusted tools where needed, you can build custom
scalars that are reliable, maintainable, and easy for clients to work with.
## Additional resources
- [GraphQL Scalars by The Guild](https://the-guild.dev/graphql/scalars): A production-ready
library of common custom scalars.
- [GraphQL Scalars Specification](https://github.com/graphql/graphql-scalars): This
specification is no longer actively maintained, but useful for historical context.