Skip to content

[Custom scalars] Handle scalar parsing during cache reads and writes - #13259

Merged
jerelmiller merged 84 commits into
feat/custom-scalarsfrom
jerel/scalar-cache-writes
Jun 10, 2026
Merged

[Custom scalars] Handle scalar parsing during cache reads and writes#13259
jerelmiller merged 84 commits into
feat/custom-scalarsfrom
jerel/scalar-cache-writes

Conversation

@jerelmiller

Copy link
Copy Markdown
Member

Part of the custom scalars work: #13227

Adds scalar value serialization for scalar fields during cache writes. This allows users to provide either the parsed or serialized value to the cache and have it work as expected.

@jerelmiller
jerelmiller marked this pull request as ready for review June 9, 2026 19:59
@jerelmiller
jerelmiller force-pushed the jerel/scalar-cache-writes branch from acd4729 to 31b8365 Compare June 9, 2026 21:33
@jerelmiller
jerelmiller force-pushed the jerel/scalar-cache-writes branch from 31b8365 to 9ba29b6 Compare June 9, 2026 21:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the InMemoryCache custom-scalars work by allowing scalar fields (configured via a new scalar field policy option) to be coerced during cache writes/merges and during cache.extract()/cache.restore(), so users can provide either parsed or serialized values and get consistent behavior.

Changes:

  • Adds scalar?: ScalarNames to field policies and plumbing to resolve a scalar per (typename, fieldName).
  • Coerces scalar values to parsed form during writes/merges, and serializes them during cache extraction (with complementary parsing during restore via the merge path).
  • Adds extensive runtime tests and TypeScript type-tests validating scalar policy typing and scalar coercion behavior.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/cache/inmemory/writeToStore.ts Coerces scalar field values during write paths (including post-merge-function).
src/cache/inmemory/readFromStore.ts Adds a DEV warning when scalar is configured on a field that actually has a selection set.
src/cache/inmemory/policies.ts Adds scalar to FieldPolicy plus scalar lookup/coercion helpers (getScalarForField, maybeCoerce*).
src/cache/inmemory/entityStore.ts Parses scalar values before merging and serializes scalar values during extract().
src/cache/inmemory/types.ts Introduces KnownScalars/ScalarNames typing helpers used by the new FieldPolicy.scalar option.
src/cache/inmemory/inMemoryCache.ts Refactors scalar-related types to reuse KnownScalars from types.ts.
src/cache/inmemory/tests/scalars.ts Adds comprehensive behavioral tests for scalar parsing/serialization across cache APIs.
integration-tests/type-tests/customScalars/mixed/index.ts Adds type-test coverage restricting FieldPolicy.scalar to known scalar names in this scenario.
integration-tests/type-tests/customScalars/matchingTypes/index.ts Adds type-test coverage for allowed/disallowed scalar names in field policies.
integration-tests/type-tests/customScalars/empty/index.ts Adds type-test coverage ensuring no scalar names are allowed when none are declared.
integration-tests/type-tests/customScalars/differentTypes/index.ts Adds type-test coverage restricting scalar names when only some scalars are declared.
integration-tests/type-tests/customScalars/all-unknown/mixed/index.ts Adds type-test coverage allowing any scalar name when scalar typing is fully unknown.
integration-tests/type-tests/customScalars/all-unknown/matchingTypes/index.ts Adds type-test coverage allowing any scalar name when scalar typing is fully unknown.
integration-tests/type-tests/customScalars/all-unknown/empty/index.ts Adds type-test coverage allowing any scalar name when scalar typing is fully unknown.
integration-tests/type-tests/customScalars/all-unknown/differentTypes/index.ts Adds type-test coverage allowing any scalar name when scalar typing is fully unknown.
integration-tests/type-tests/customScalars/all-structured/mixed/index.ts Adds type-test coverage allowing any scalar name in the “all-structured” scenario.
integration-tests/type-tests/customScalars/all-structured/matchingTypes/index.ts Adds type-test coverage allowing any scalar name in the “all-structured” scenario.
integration-tests/type-tests/customScalars/all-structured/empty/index.ts Adds type-test coverage allowing any scalar name in the “all-structured” scenario.
integration-tests/type-tests/customScalars/all-structured/differentTypes/index.ts Adds type-test coverage allowing any scalar name in the “all-structured” scenario.
integration-tests/type-tests/customScalars/all-any/mixed/index.ts Adds type-test coverage allowing any scalar name when scalar typing is any.
integration-tests/type-tests/customScalars/all-any/matchingTypes/index.ts Adds type-test coverage allowing any scalar name when scalar typing is any.
integration-tests/type-tests/customScalars/all-any/empty/index.ts Fixes imports and adds type-test coverage allowing any scalar name when scalar typing is any.
integration-tests/type-tests/customScalars/all-any/differentTypes/index.ts Adds type-test coverage allowing any scalar name when scalar typing is any.
.changeset/strong-shoes-sell.md Documents the new FieldPolicy.scalar option and its effect on reads/writes/extract/restore.
.api-reports/api-report.api.md Updates API report for new public types/methods introduced by scalar coercion support.
.api-reports/api-report-cache.api.md Updates cache API report for the new FieldPolicy.scalar option and Policies APIs.

Comment thread src/cache/inmemory/readFromStore.ts
Comment thread src/cache/inmemory/entityStore.ts
Comment thread src/cache/inmemory/policies.ts Outdated
Comment on lines +912 to +915
public maybeCoerceToScalarValue(
value: StoreValue,
options: CoerceValueOptions
): StoreValue {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out I don't need these anymore now that all of the logic lives in EntityStore. Removed in 03945ef

Comment thread src/cache/inmemory/writeToStore.ts Outdated
Comment thread src/cache/inmemory/entityStore.ts
@jerelmiller
jerelmiller requested a review from DaleSeo June 10, 2026 15:55
@github-actions github-actions Bot added the auto-cleanup 🤖 label Jun 10, 2026
@jerelmiller
jerelmiller force-pushed the jerel/scalar-cache-writes branch from adcaf51 to 260a649 Compare June 10, 2026 16:04
Base automatically changed from jerel/custom-scalar-config to feat/custom-scalars June 10, 2026 16:04
@jerelmiller
jerelmiller merged commit ccaf686 into feat/custom-scalars Jun 10, 2026
44 of 46 checks passed
@jerelmiller
jerelmiller deleted the jerel/scalar-cache-writes branch June 10, 2026 16:31
jerelmiller added a commit that referenced this pull request Jun 10, 2026
…13259)

_Part of the custom scalars work:
https://github.com/apollographql/apollo-client/issues/13227_

Adds scalar value serialization for scalar fields during cache writes.
This allows users to provide either the parsed or serialized value to
the cache and have it work as expected.
coerce: (scalar: Scalar<any, any>, value: unknown) => unknown,
typename = obj.__typename
): StoreObject {
if (!typename) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jerelmiller could we add an early bailout for a "no scalars defined" case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.policies.rootTypenamesById[dataId]
);

const merged: StoreObject = new DeepMerger({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does DeepMerger need new safeguards so it doesn't smush together the implementation details of user-defined distinct scalar values?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so? We would have had this problem already I would think since you still have the possibility of e.g. object scalars (like JSON), and that seems to be working fine 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would assume that this has been wonky all the time until now, and nobody ever noticed/reported it.

jerelmiller added a commit that referenced this pull request Jun 30, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to release-4.3, this
PR will be updated.

⚠️⚠️⚠️⚠️⚠️⚠️

`release-4.3` is currently in **pre mode** so this branch has
prereleases rather than normal releases. If you want to exit
prereleases, run `changeset pre exit` on `release-4.3`.

⚠️⚠️⚠️⚠️⚠️⚠️

# Releases
## @apollo/client@4.3.0-alpha.2

### Minor Changes

- [#13274](#13274)
[`7b10078`](7b10078)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds
`Scalar.fromGraphQLScalarType` helper to create a `Scalar` instance from
an existing graphql.js `GraphQLScalarType`.

  ```ts
  import { GraphQLScalarType } from "graphql";
  import { Scalar } from "@apollo/client";

  const dateTimeScalarType = new GraphQLScalarType<Date, string>({
    // ...
  });

const dateTimeScalar = Scalar.fromGraphQLScalarType(dateTimeScalarType,
{
    is: (value) => value instanceof Date,
  });
  ```

- [#13252](#13252)
[`ed86234`](ed86234)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds the
plumbing and types implementation for declaring custom scalars and
configuring custom scalars in `InMemoryCache`.

You can declare custom scalar types with declaration merging on the
`ApolloCache.Scalars` interface:

  ```ts
  // apollo.d.ts
  import "@apollo/client";

  declare module "@apollo/client" {
    namespace ApolloCache {
      interface Scalars {
        Date: { serialized: string; parsed: Date };
      }
    }
  }
  ```

  This enables the `scalars` option in `InMemoryCache`:

  ```ts
  import { Scalar } from "@apollo/client";

  const cache = new InMemoryCache({
    scalars: {
      Date: new Scalar({
        parse: (dateString) => new Date(dateString),
        serialize: (date) => date.toISOString(),
        is: (value) => value instanceof Date,
      }),
    },
  });
  ```

- [#13259](#13259)
[`ccaf686`](ccaf686)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds a `scalar`
option to `InMemoryCache` field policies that tells the cache which
scalar to use when parsing or serializing the field value.

  ```ts
  import { Scalar } from "@apollo/client";

  new InMemoryCache({
    scalars: {
      DateTime: new Scalar({
        parse: (dateString) => new Date(dateString),
        serialize: (date) => date.toISOString(),
      }),
    },
    typePolicies: {
      Event: {
        fields: {
          startTime: {
            // Parse this field using the DateTime scalar
            scalar: "DateTime",
          },
        },
      },
    },
  });
  ```

This scalar definition is now used to properly parse or serialize the
field value for cache reads and writes as well as `cache.extract()` and
`cache.restore()`.

- [#13273](#13273)
[`0886de1`](0886de1)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Automatically
serialize variables that include custom scalar values. This includes
cache reads and writes as well as requests to the network.

For more complex input objects, a new `inputObjects` option is available
to `InMemoryCache` that specifies where nested scalar fields are found.

  ```ts
  const cache = new InMemoryCache({
    scalars: {
      DateTime: new Scalar({
        parse: (value) => new Date(value),
        serialize: (value) => value.toISOString(),
        is: (value) => value instanceof Date,
      }),
    },
    inputObjects: {
      EventFilter: {
        fields: {
          date: "DateTime",
        },
      },
    },
  });

  const client = new ApolloClient({ cache, link });

  await client.query({
    query: gql`
      query Event($filter: EventFilter!) {
        event(filter: $filter) {
          name
        }
      }
    `,
    variables: {
      filter: {
        date: new Date("2026-01-01T00:00:00.000Z"),
      },
    },
  });

  // The link receives:
  // { filter: { date: "2026-01-01T00:00:00.000Z" } }
  ```

- [#13252](#13252)
[`ed86234`](ed86234)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds the
`getScalar` abstract method to `ApolloCache` that cache subclasses
override to provide scalar behavior to Apollo Client. Defaults to
unconditionally return `undefined` if not specified.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jerel Miller <jerelmiller@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants