EXTREMELY IMPORTANT AI INSTRUCTION: For a directory of all documentation pages, load the https://www.apollographql.com/docs/llms.txt file. Adding .md to ANY url will return the simplified markdown version of the page.
Custom scalars
Parse and serialize GraphQL scalar values in Apollo Client
GraphQL schemas often define custom scalar types to add semantic meaning to a field that a built-in type can't convey. For example, a createdAt field might be defined as a DateTime scalar instead of a String. Apollo Client can parse those values into types that are easier to work with in your application, such as Date objects.
This guide walks through using custom scalars in your own applications.
Define a Scalar
Define custom scalars with the Scalar class. A Scalar describes how to convert a value between the JSON serialized value and the parsed value:
1import { Scalar } from "@apollo/client";
2
3const dateTimeScalar = new Scalar<string, Date>({
4 parse: (dateString) => new Date(dateString),
5 serialize: (date) => date.toISOString(),
6 is: (value) => value instanceof Date,
7});Scalar accepts the following options:
(
value: TSerialized | TParsed
) => booleanA predicate function that determines whether the value
is the parsed value. Return true when the value is the parsed value.
1 new Scalar<string, Date>({
2 is: (value) => value instanceof Date,
3 });(
serializedValue: TSerialized
) => NoInfer<TParsed>A function that transforms the JSON serialized value into its parsed value.
1 new Scalar<string, Date>({
2 parse: (dateString) => new Date(dateString),
3 });(
parsedValue: TParsed
) => NoInfer<TSerialized>A function that transforms the parsed value into its JSON serialized value.
1 new Scalar<string, Date>({
2 serialize: (date) => date.toISOString(),
3 });The is function
The is function determines whether a value is in its parsed or serialized form. Apollo Client uses this method to coerce values as needed during the lifetime of your application. When is returns true, Apollo Client treats the value as the parsed type. When is returns false, Apollo Client treats the value as the serialized type.
is, Apollo Client treats any non-null object as a parsed value. That default might be enough, but it is typically better to pass your own is.Create a Scalar from a GraphQL.js GraphQLScalarType
If you have a GraphQLScalarType instance from graphql, you can use it to create an Apollo Client compatible Scalar using Scalar.fromGraphQLScalarType. Apollo Client uses parseValue and serialize from the GraphQL.js scalar.
1import { GraphQLScalarType } from "graphql";
2import { Scalar } from "@apollo/client";
3
4const dateTime = new GraphQLScalarType({
5 name: "DateTime",
6 parseValue: (value) => new Date(value),
7 serialize: (value) => value.toISOString(),
8});
9
10const dateTimeScalar = Scalar.fromGraphQLScalarType(dateTime, {
11 is: (value) => value instanceof Date,
12});This is most useful when you want to use scalar types from shared NPM packages such as graphql-scalars, which provide many useful scalar implementations out of the box:
1import { GraphQLDateTimeISO } from "graphql-scalars";
2
3const dateTimeScalar = Scalar.fromGraphQLScalarType(GraphQLDateTimeISO, {
4 is: (value) => value instanceof Date,
5});graphql-scalars is published primarily for GraphQL servers, but you can use it on the client. Some scalars may depend on Node APIs. Use only scalars that work in your environment, such as the browser or React Native.Configure the cache
Pass your Scalar instances to the scalars option on InMemoryCache. Map each instance to the name of the scalar in your schema:
1new InMemoryCache({
2 scalars: {
3 DateTime: dateTimeScalar,
4 URL: urlScalar,
5 // ...
6 },
7});The scalars option registers the implementations. You still need to tell the cache which fields and input object fields use those scalars.
Map fields to scalars
Passing scalars to InMemoryCache isn't enough for the cache to know which fields should be associated with the scalars because InMemoryCache has no schema knowledge. Set the scalar property on a field policy to associate the field with the scalar type:
1new InMemoryCache({
2 scalars: {
3 DateTime: dateTimeScalar,
4 URL: urlScalar,
5 },
6 typePolicies: {
7 Event: {
8 fields: {
9 createdAt: {
10 scalar: "DateTime",
11 },
12 updatedAt: {
13 scalar: "DateTime",
14 },
15 siteLink: {
16 scalar: "URL",
17 },
18 },
19 },
20 },
21});scalar, Apollo Client ignores read and merge functions on that field and emits a development-only warning.Each field associated with a custom scalar needs a field policy with a scalar property. If you want to avoid writing these by hand, see the generate scalar configuration section to generate this configuration from your schema.
With the field policies in place, query, mutation, and subscription results return parsed values for mapped fields.
1const QUERY = gql`
2 query GetEvent($id: ID!) {
3 event(id: $id) {
4 id
5 name
6 siteLink
7 createdAt
8 updatedAt
9 }
10 }
11`;
12
13const { data } = useQuery(QUERY, { variables: { id } });
14// => {
15// event: {
16// id: "1",
17// name: "GraphQL Conf",
18// siteLink: URL,
19// createdAt: Date,
20// updatedAt: Date,
21// },
22// }
23
24const MUTATION = gql`
25 mutation CreateEvent($input: CreateEventInput!) {
26 createEvent(input: $input) {
27 id
28 createdAt
29 }
30 }
31`;
32
33const { data } = client.mutate({
34 mutation: MUTATION,
35 variables: {
36 /*...*/
37 },
38});
39// => {
40// createEvent: {
41// id: "1",
42// createdAt: Date,
43// },
44// }no-cache fetch policies. Apollo Client converts scalar fields before returning the result.Cache read and write APIs also use parsed values, including readQuery, readFragment, writeQuery, and writeFragment:
1const data = client.readQuery({ query: QUERY, variables: { id } });
2// => {
3// event: {
4// id: "1",
5// name: "GraphQL Conf",
6// siteLink: URL,
7// createdAt: Date,
8// updatedAt: Date,
9// },
10// }Lists of scalars
Some fields return a list of custom scalar values, such as [DateTime!]. Use GraphQL list syntax so Apollo Client parses and serializes each element as that scalar:
1new InMemoryCache({
2 scalars: {
3 DateTime: dateTimeScalar,
4 },
5 typePolicies: {
6 Event: {
7 fields: {
8 createdAt: {
9 scalar: "DateTime",
10 },
11 meetingTimes: {
12 scalar: "[DateTime]",
13 },
14 availabilitySlots: {
15 scalar: "[[DateTime]]",
16 },
17 },
18 },
19 },
20});Use one pair of brackets per list level so Apollo Client iterates each item at the right depth.
!) in the value because it won't match. Write "[DateTime]", not "[DateTime!]!"Query results then return parsed values for each element:
1const QUERY = gql`
2 query GetEvent($id: ID!) {
3 event(id: $id) {
4 id
5 createdAt
6 meetingTimes
7 availabilitySlots
8 }
9 }
10`;
11
12const { data } = useQuery(QUERY, { variables: { id } });
13// => {
14// event: {
15// id: "1",
16// createdAt: Date,
17// meetingTimes: [Date, Date],
18// availabilitySlots: [[Date, Date], [Date]],
19// },
20// }The same list syntax applies to inputObjects field mappings.
"[DateTime]" but the value is not an array, Apollo Client still coerces the value as DateTime and emits a development-only warning.Scalars that serialize as arrays
Some custom scalars use a JSON array as the serialized form, even though the GraphQL field is not a list. For example, a DateTimeRange scalar might serialize as ["2026-01-01T00:00:00.000Z", "2026-06-01T00:00:00.000Z"].
Do not wrap that scalar in list syntax. Pass the scalar name so Apollo Client gives the whole array to parse and serialize:
1const dateTimeRangeScalar = new Scalar<
2 [string, string],
3 { start: Date; end: Date }
4>({
5 parse: ([start, end]) => ({
6 start: new Date(start),
7 end: new Date(end),
8 }),
9 serialize: (range) => [range.start.toISOString(), range.end.toISOString()],
10 is: (value) => !Array.isArray(value),
11});
12
13new InMemoryCache({
14 scalars: {
15 DateTimeRange: dateTimeRangeScalar,
16 },
17 typePolicies: {
18 Event: {
19 fields: {
20 dateRange: {
21 scalar: "DateTimeRange",
22 },
23 dateRanges: {
24 scalar: "[DateTimeRange]",
25 },
26 },
27 },
28 },
29});"[DateTimeRange]" is a list of DateTimeRange values. Apollo Client iterates the outer array, then passes each inner array to the DateTimeRange Scalar instance.
is, Apollo Client treats any non-null object as a parsed value, including JSON arrays. Provide an is function that returns false for the serialized array so Apollo Client still calls parse.Variable serialization
Apollo Client serializes parsed scalar values in variables before it sends the request to your server and before it reads or writes the cache with those variables. You can pass parsed values in any API that accepts a variables option:
1const QUERY = gql`
2 query GetEventsOnDate($date: DateTime!) {
3 events(on: $date) {
4 id
5 name
6 }
7 }
8`;
9
10const { data } = useQuery(QUERY, {
11 variables: { date: new Date("2026-01-01T00:00:00.000Z") },
12});
13
14// The link receives `{ date: "2026-01-01T00:00:00.000Z" }`Cache APIs that take variables serialize them the same way:
1client.readQuery({
2 query: QUERY,
3 variables: { date: new Date("2026-01-01T00:00:00.000Z") },
4});
5
6client.writeQuery({
7 query: QUERY,
8 data: {
9 /* ... */
10 },
11 variables: { date: new Date("2026-01-01T00:00:00.000Z") },
12});Apollo Client reads the GraphQL type from the variable definition ($date: DateTime!) and calls serialize on the configured DateTime scalar.
Apollo Client also reads list types from the variable definition. You do not need extra cache configuration for a [DateTime] variable. Apollo Client serializes each element:
1const QUERY = gql`
2 query GetEventsOnDates($dates: [DateTime!]!) {
3 events(on: $dates) {
4 id
5 name
6 }
7 }
8`;
9
10const { data } = useQuery(QUERY, {
11 variables: {
12 dates: [
13 new Date("2026-01-01T00:00:00.000Z"),
14 new Date("2026-01-02T00:00:00.000Z"),
15 ],
16 },
17});
18
19// The link receives `{ dates: ["2026-01-01T00:00:00.000Z", "2026-01-02T00:00:00.000Z"] }`Input objects
Input objects can contain custom scalar fields. Those nested fields are not visible from the variable definition alone. For example, the following query has a $dateRange variable of type DateRangeFilter:
1query GetEventsInRange($dateRange: DateRangeFilter!) {
2 events(filter: $dateRange) {
3 id
4 name
5 }
6}To serialize custom scalars in input objects, pass an inputObjects option that maps each input object's fields to the associated custom scalar type:
1new InMemoryCache({
2 inputObjects: {
3 DateRangeFilter: {
4 fields: {
5 start: "DateTime",
6 end: "DateTime",
7 },
8 },
9 },
10});You can then pass parsed scalar values in variables:
1const QUERY = gql`
2 query GetEventsInRange($dateRange: DateRangeFilter!) {
3 events(filter: $dateRange) {
4 id
5 name
6 }
7 }
8`;
9
10const { data } = useQuery(QUERY, {
11 variables: {
12 dateRange: {
13 start: new Date("2026-01-01T00:00:00.000Z"),
14 end: new Date("2026-02-01T00:00:00.000Z"),
15 },
16 },
17});Input object fields can also be lists of custom scalars. Use the same GraphQL list syntax described in Lists of scalars:
1new InMemoryCache({
2 inputObjects: {
3 AvailabilityInput: {
4 fields: {
5 dates: "[DateTime]",
6 slots: "[[DateTime]]",
7 },
8 },
9 },
10});Deeply nested input objects
Input objects can contain other input objects that map their own custom scalars. When the input object is more than one level deep, pass the name of the nested input object for the field that references it:
1new InMemoryCache({
2 inputObjects: {
3 DateRangeFilter: {
4 fields: {
5 start: "DateTime",
6 end: "DateTime",
7 },
8 },
9 EventFilter: {
10 fields: {
11 dateRange: "DateRangeFilter",
12 },
13 },
14 ConferenceFilter: {
15 fields: {
16 when: "DateRangeFilter",
17 },
18 },
19 },
20});You can then pass parsed scalar values in variables:
1const QUERY = gql`
2 query GetData(
3 $eventFilter: EventFilter!
4 $conferenceFilter: ConferenceFilter!
5 ) {
6 events(filter: $eventFilter) {
7 id
8 name
9 }
10 conferences(filter: $conferenceFilter) {
11 id
12 name
13 }
14 }
15`;
16
17const { data } = useQuery(QUERY, {
18 variables: {
19 eventFilter: {
20 dateRange: {
21 start: new Date("2026-01-01T00:00:00.000Z"),
22 end: new Date("2026-02-01T00:00:00.000Z"),
23 },
24 },
25 conferenceFilter: {
26 when: {
27 start: new Date("2026-01-01T00:00:00.000Z"),
28 end: new Date("2026-02-01T00:00:00.000Z"),
29 },
30 },
31 },
32});Convert values outside of Apollo Client
Sometimes you need to convert a value with a Scalar you already configured. You can access the scalar instance with cache.getScalar(), which takes a key that matches a scalar you passed to the scalars option and returns the Scalar instance that matches that name.
Use it when you have a raw value from outside Apollo Client that you want to transform with that Scalar. For example, a query param:
1const dateTime = client.cache.getScalar("DateTime");
2
3const start = dateTime.parse(searchParams.get("start"));
4
5url.searchParams.set("start", dateTime.serialize(event.createdAt));cache.getScalar() for values provided to Apollo Client APIs. Apollo Client already parses or serializes scalar values as needed.Coerce parsed and serialized values
If you aren't sure whether a value is parsed or serialized, use coerceToParsed or coerceToSerialized. These methods use is to check the form of the value, then parse or serialize only when needed.
1function formatDate(date: string | Date) {
2 const dateTimeScalar = client.cache.getScalar("DateTime");
3
4 return dateTimeScalar.coerceToParsed(date).toLocaleDateString();
5}A note about server-side rendering
Custom scalars are safe to use with server-side rendering. cache.extract() serializes parsed values to JSON so you can send the cache to the client. cache.restore() parses those values again, so both the server and the client keep working with the parsed types.
1// On the server
2const initialState = cache.extract();
3// => {
4// ROOT_QUERY: {
5// __typename: "Query",
6// event: { __ref: "Event:1" },
7// },
8// "Event:1": {
9// __typename: "Event",
10// id: "1",
11// name: "GraphQL Conf",
12// siteLink: "https://graphqlconf.org/",
13// createdAt: "2026-01-01T00:00:00.000Z",
14// updatedAt: "2026-01-01T00:00:00.000Z",
15// },
16// }
17
18// On the client
19new InMemoryCache({
20 scalars: {
21 DateTime: dateTimeScalar,
22 URL: urlScalar,
23 },
24}).restore(initialState);TypeScript
Register custom scalars with TypeScript to add type safety for the scalars option on InMemoryCache and the cache.getScalar() API. Declare your custom scalars using TypeScript's declaration merging with the ApolloCache.Scalars interface.
1// This import is necessary to ensure all Apollo Client imports
2// are still available to the rest of the application.
3import "@apollo/client";
4
5declare module "@apollo/client" {
6 namespace ApolloCache {
7 interface Scalars {
8 DateTime: { serialized: string; parsed: Date };
9 }
10 }
11}String, Int, Float, Boolean, or ID. Apollo Client already handles those types.TypeScript checks the scalars option when you create InMemoryCache:
1// ❌ invalid: missing `scalars` option
2new InMemoryCache();
3// ❌ invalid: missing `DateTime` scalar
4new InMemoryCache({ scalars: {} });
5
6new InMemoryCache({
7 scalars: {
8 // ❌ invalid: unknown scalar "Unknown"
9 Unknown: new Scalar(/*...*/),
10
11 // ❌ invalid: Scalar<number, string> not assignable to Scalar<string, Date>
12 DateTime: new Scalar<number, string>(/*...*/),
13
14 // ✅ valid
15 DateTime: new Scalar<string, Date>(/*...*/),
16 },
17});serialized and parsed are the same type, the entry in scalars is optional. For example, a Year scalar might define both the parsed and serialized types as a number. In that case, your app doesn't need a Scalar instance for it.scalars option on InMemoryCache. It doesn't affect the types set for scalar fields returned by operation data. See the Generate scalar configuration section for more information.Generate scalar configuration
Writing a field policy or input object entry for every custom scalar field is tedious and error prone. Use the @apollo/client-graphql-codegen/custom-scalars plugin to generate the type policies and input objects configuration from your schema.
Installation
Install the Apollo Client GraphQL Codegen package:
npm install -D @apollo/client-graphql-codegenConfiguration
Add the custom-scalars plugin to your codegen.ts configuration:
1import type { CodegenConfig } from "@graphql-codegen/cli";
2import type { CustomScalarsPluginConfig } from "@apollo/client-graphql-codegen/custom-scalars";
3
4const config: CodegenConfig = {
5 // ... your existing configuration
6 generates: {
7 // ... your existing generates
8 "./src/__generated__/custom-scalars.ts": {
9 plugins: ["@apollo/client-graphql-codegen/custom-scalars"],
10 config: {
11 // Optional. See plugin options below.
12 } satisfies CustomScalarsPluginConfig,
13 },
14 },
15};
16
17export default config;The generated file exports inputObjects and scalarTypePolicies.
Example generated file
1import type { InputObjectsOption, TypePolicies } from "@apollo/client/cache";
2
3export const inputObjects: InputObjectsOption = {
4 DateRangeFilter: {
5 fields: {
6 start: "DateTime",
7 end: "DateTime",
8 },
9 },
10 AvailabilityInput: {
11 fields: {
12 dates: "[DateTime]",
13 },
14 },
15};
16
17export const scalarTypePolicies: TypePolicies = {
18 Event: {
19 fields: {
20 createdAt: {
21 scalar: "DateTime",
22 },
23 updatedAt: {
24 scalar: "DateTime",
25 },
26 meetingTimes: {
27 scalar: "[DateTime]",
28 },
29 availabilitySlots: {
30 scalar: "[[DateTime]]",
31 },
32 siteLink: {
33 scalar: "URL",
34 },
35 },
36 },
37};This plugin generates cache configuration only. It does not change the TypeScript types for your operations. To type operation results as parsed values, add a scalars mapping to the configuration that creates your operation types:
1const config: CodegenConfig = {
2 generates: {
3 "./src/types/__generated__/graphql.ts": {
4 plugins: ["typescript-operations"],
5 config: {
6 scalars: {
7 DateTime: "Date",
8 URL: "URL",
9 },
10 // ...other options
11 },
12 },
13 },
14};See the GraphQL Codegen scalars documentation for more information.
Configure InMemoryCache from codegen
Import the generated inputObjects and scalarTypePolicies objects and pass them to InMemoryCache.
1import {
2 inputObjects,
3 scalarTypePolicies,
4} from "./path/to/__generated__/custom-scalars";
5
6const cache = new InMemoryCache({
7 scalars: {
8 DateTime: dateTimeScalar,
9 URL: urlScalar,
10 },
11 inputObjects,
12});
13
14cache.policies.addTypePolicies(scalarTypePolicies);scalarTypePolicies to cache.policies.addTypePolicies(), which merges the generated scalar options with any other field policies you define, such as keyArgs or keyFields.Plugin options
By default, the plugin includes only custom scalar fields and input objects that your GraphQL documents use. You can change this behavior with the following plugin options:
| Name / Type |
Description |
|---|---|
| If true, the generated config includes only custom scalar fields selected by your documents and input objects reachable from those documents' variables.The default value is true. Set this option to false to generate config for every custom scalar field in the schema. |
| Allowed list of scalars that should be included in the config objects. You can reduce the size of the config objects by omitting fields with scalars that won't have an associated scalar transform. This option is mutually exclusive with ignoreScalars. |
| List of scalars that should be ignored when generating the config objects. You can reduce the size of the config objects by omitting fields with scalars that won't have an associated scalar transform. This option is mutually exclusive with includeScalars. |