Apollo Client (Web)
IntroductionWhy Apollo Client?Get started
Core concepts
Caching
Pagination
Local State
Development & Testing
Performance
Integrations
Networking
API Reference
ChangelogMigrating to Apollo Client 4.0Versioning Policy

Custom scalars

Parse and serialize GraphQL scalar values in Apollo Client

Requires ≥ 4.3

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:

TypeScript
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 ) => boolean

A predicate function that determines whether the value is the parsed value. Return true when the value is the parsed value.

TypeScript
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.

TypeScript
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.

TypeScript
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.

note
If you omit 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.

TypeScript
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:

TypeScript
1import { GraphQLDateTimeISO } from "graphql-scalars";
2
3const dateTimeScalar = Scalar.fromGraphQLScalarType(GraphQLDateTimeISO, {
4  is: (value) => value instanceof Date,
5});
note
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:

TypeScript
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.

note
To type-check this configuration, see the TypeScript section to declare your custom scalar types.

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:

TypeScript
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});
caution
If a field policy sets 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.

TypeScript
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// }
note
Even though scalar configuration is defined in the cache, you can safely use 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:

TypeScript
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:

TypeScript
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.

caution
Do not include the GraphQL non-null marker (!) in the value because it won't match. Write "[DateTime]", not "[DateTime!]!"

Query results then return parsed values for each element:

TypeScript
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.

note
If a field is configured as a list type such as "[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:

TypeScript
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.

caution
If you omit 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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

GraphQL
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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});
tip
Instead of writing this configuration by hand, you can generate it with GraphQL Codegen. See the generate scalar configuration section.

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:

TypeScript
1const dateTime = client.cache.getScalar("DateTime");
2
3const start = dateTime.parse(searchParams.get("start"));
4
5url.searchParams.set("start", dateTime.serialize(event.createdAt));
note
You do not need 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.

TypeScript
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.

TypeScript
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.

TypeScript
apollo.d.ts
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}
note
You don't need to declare built-in GraphQL scalars such as String, Int, Float, Boolean, or ID. Apollo Client already handles those types.

TypeScript checks the scalars option when you create InMemoryCache:

TypeScript
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});
note
When 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.
note
This declaration types the 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:

sh
npm install -D @apollo/client-graphql-codegen
tip
See the GraphQL Codegen guide to learn how to use GraphQL Codegen with Apollo Client if you don't have it set up.

Configuration

Add the custom-scalars plugin to your codegen.ts configuration:

TypeScript
codegen.ts
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
TypeScript
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:

TypeScript
codegen.ts
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.

TypeScript
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);
tip
We recommend passing 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
filterByDocuments
boolean
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.
includeScalars
string[]
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.
ignoreScalars
string[]
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.

See also