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:
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)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:
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:
<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.
Use <ApolloFragment> inside the component that owns the fragment. The masked object the parent passed down goes straight into from:
<script setup lang="ts">
import { ApolloFragment } from '@vue/apollo-components'
const { post } = defineProps<{
post: { __typename: 'Post', id: string }
}>()
</script>
<template>
<ApolloFragment
:fragment="gql`
fragment PostDetailsFragment on Post {
title
publishedAt
}
`"
:from="post"
>
<template #data="{ data }">
<h2>{{ data.title }}</h2>
<p>{{ data.publishedAt }}</p>
</template>
</ApolloFragment>
</template>data only contains the fields defined in the fragment. Parent queries do not leak into it, and sibling fragments do not leak either.
With masking enabled, a component cannot read the fragment's fields from the post prop it receives. <ApolloFragment> unmasks them in the template.
Fixing the parent component ​
With data masking on, the parent must explicitly request any fields it needs:
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:
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:
query GetPosts {
posts {
id
...PostDetailsFragment @unmask(mode: "migrate")
}
}This helps you find every implicit dependency before removing the @unmask directive.
What gets masked ​
| API | Masked |
|---|---|
useQuery result | Yes |
useMutation result | Yes |
useSubscription result | Yes |
useMutation update callback | No |
useMutation refetchQueries callback | No |
subscribeToMore updateQuery callback | No |
Cache APIs (readQuery, readFragment) | No |
Every row above applies to the components too. A masked field is missing from <ApolloQuery>'s #data slot and from its @result payload, and still present inside an update callback passed through <ApolloMutation>'s options prop.
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:
query GetPosts {
posts {
id
...PostDetailsFragment @unmask(mode: "migrate")
}
}2. Enable data masking ​
const client = new ApolloClient({
dataMasking: true,
// ...
})3. Configure TypeScript (if applicable) ​
If you use TypeScript with GraphQL Codegen:
- Update your codegen config to emit masked types (TypeScript setup).
- Add the type augmentation file (Enabling data masking types).
4. Refactor components ​
- Watch the console for "accessing masked field" warnings.
- Read the data a component owns through
useFragmentor<ApolloFragment>. - Add any required fields to parent queries explicitly.
- Remove
@unmaskdirectives when no warnings remain.
Next steps ​
- Fragments covers fragment basics and
useFragment. - TypeScript sets up GraphQL Codegen for type-safe masked data.