Skip to content

Data Masking ​

Data masking prevents components from accessing GraphQL fields they did not explicitly request. The result is loosely coupled components that are more resistant to breaking changes when fragments evolve.

Recommended: use with GraphQL Codegen

Data masking works best with GraphQL Codegen so that masked types match runtime behavior. See TypeScript for setup, including the required type augmentation.

The problem masking solves ​

Consider a parent component that fetches posts and a child component that displays post details:

ts
const 
GET_POSTS
=
gql
`
query GetPosts { posts { id ...PostDetailsFragment } } ${
POST_DETAILS_FRAGMENT
}
` const {
current
} =
useQuery
(
GET_POSTS
)
// Filter by publishedAt (defined in PostDetailsFragment, not in this query directly) const
published
=
current
.
value
.
result
?.
posts
.
filter
(
post
=>
post
.
publishedAt
)
ts
export const 
POST_DETAILS_FRAGMENT
=
gql
`
fragment PostDetailsFragment on Post { title publishedAt } `

The parent reads publishedAt, but publishedAt is defined in the child's fragment. If PostDetails later removes publishedAt from its fragment (because it no longer displays it), the parent breaks silently. This implicit dependency between components grows harder to track as the app grows.

Enabling data masking ​

Pass dataMasking: true to the Apollo Client constructor:

ts
import { 
ApolloClient
,
HttpLink
,
InMemoryCache
} from '@apollo/client'
const
client
= new
ApolloClient
({
link
: new
HttpLink
({
uri
: 'https://api.example.com/graphql' }),
cache
: new
InMemoryCache
(),
dataMasking
: true,
})

With masking on, fields defined only in a fragment are hidden from queries that include that fragment. The parent can only see the fields it requested directly.

Reading masked data ​

Use useFragment inside the component that owns the fragment:

vue
<script setup lang="ts">
import { 
useFragment
} from '@vue/apollo-composable'
const {
post
} =
defineProps
<{
post
: {
__typename
: 'Post',
id
: string }
}>() const {
current
} =
useFragment
({
fragment
:
POST_DETAILS_FRAGMENT
,
from
: () =>
post
,
}) </script> <template> <
div
v-if="
current
.
resultState
=== 'complete'">
<
h2
>{{
current
.
result
.
title
}}</
h2
>
<
p
>{{
current
.
result
.
publishedAt
}}</
p
>
</
div
>
</template>

current.result only contains the fields defined in the fragment. Parent queries do not leak into it, and sibling fragments do not leak either.

Fixing the parent component ​

With data masking on, the parent must explicitly request any fields it needs:

ts
const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      publishedAt    # Explicit, no longer dependent on the child fragment
      ...PostDetailsFragment
    }
  }
  ${POST_DETAILS_FRAGMENT}
`

Now if PostDetails removes publishedAt, the parent query still works because it requests the field directly.

The @unmask directive ​

@unmask selectively disables masking for a specific fragment spread:

graphql
query GetPosts {
  posts {
    id
    ...PostDetailsFragment @unmask
  }
}

Use sparingly

@unmask is an escape hatch. Prefer adding the needed fields to the parent query explicitly. Reach for @unmask only during incremental adoption.

Migrate mode ​

While migrating, @unmask(mode: "migrate") logs a development warning whenever you read a field that would otherwise be masked:

graphql
query GetPosts {
  posts {
    id
    ...PostDetailsFragment @unmask(mode: "migrate")
  }
}

This helps you find every implicit dependency before removing the @unmask directive.

What gets masked ​

APIMasked
useQuery resultYes
useMutation resultYes
useSubscription resultYes
useMutation update callbackNo
useMutation refetchQueries callbackNo
subscribeToMore updateQuery callbackNo
Cache APIs (readQuery, readFragment)No

Cache APIs and mutation update callbacks deal with the underlying cache directly, so masking does not apply to them.

Incremental adoption ​

You can adopt data masking gradually in an existing codebase:

1. Add @unmask(mode: "migrate") everywhere ​

Before enabling dataMasking, add @unmask(mode: "migrate") to every fragment spread so nothing breaks:

graphql
query GetPosts {
  posts {
    id
    ...PostDetailsFragment @unmask(mode: "migrate")
  }
}

2. Enable data masking ​

ts
const client = new ApolloClient({
  dataMasking: true,
  // ...
})

3. Configure TypeScript (if applicable) ​

If you use TypeScript with GraphQL Codegen:

  1. Update your codegen config to emit masked types (TypeScript setup).
  2. Add the type augmentation file (Enabling data masking types).

4. Refactor components ​

  1. Watch the console for "accessing masked field" warnings.
  2. Update components to use useFragment for the data they own.
  3. Add any required fields to parent queries explicitly.
  4. Remove @unmask directives when no warnings remain.

Next steps ​

  • Fragments covers fragment basics and useFragment.
  • TypeScript sets up GraphQL Codegen for type-safe masked data.

Released under the MIT License.