Behavior difference between Sanity client and vision tool when referencing documents with a modified _id.

4 replies
Last updated: Apr 22, 2024
Hello all, question regarding the _id of documents. We recently did a migration in sanity and decided we wanted to be able to know the “old” _id by making the new document id be
prefix.old__id
. This works fine however if we reference these documents in a groq query we get results in the vision tool (any setting really) but if we use the sanity client it will return null?!
AI Update

This is a known behavior in Sanity related to how document IDs with periods (dots) are handled. Based on the IDs and Paths documentation, here's what's happening:

The Issue

When you create document IDs with the format prefix.old__id, you're creating what Sanity considers a subpath document. According to the documentation:

Any document ID containing a dot is considered private and has restricted accessibility

All documents that contain a . in their _id can only be accessed when a user is logged in or a valid authentication token is provided for client and HTTP API calls (minimum read permission required).

This explains why Vision works but your client returns null - Vision runs in an authenticated context within the Studio, while your client queries may not be properly authenticated.

Why Vision Works But Client Doesn't

  1. Vision Tool: Runs within the Studio with your authentication credentials automatically included
  2. Sanity Client: Needs explicit authentication to access documents with dots in their IDs

Solutions

Option 1: Add Authentication to Your Client (If you need private documents)

import {createClient} from '@sanity/client'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset',
  apiVersion: '2024-01-01',
  token: 'your-read-token', // Add a token with at least read permissions
  useCdn: false, // Important: CDN doesn't work with authenticated requests
})

Instead of using dots in the _id, store the old ID as a custom field in your schema. This is the recommended approach in the documentation:

// In your schema
defineField({
  name: 'oldId',
  title: 'Old ID',
  type: 'string',
})

Then during migration, set the _id to a standard Sanity UUID and store your prefix.old__id value in the oldId field:

{
  _id: uuid(), // Standard Sanity ID
  oldId: 'prefix.old__id', // Your migration reference
  // ... other fields
}

Query it like this:

*[oldId == "prefix.old__id"]

Option 3: Replace Dots with Another Character

If you must preserve the ID structure in the _id itself, use underscores or hyphens instead:

  • prefix_old__id
  • prefix-old--id

These will work as public document IDs without authentication.

Why This Restriction Exists

The dot-based restriction exists because Sanity uses dots for internal document paths:

  • drafts.documentId for draft documents
  • versions.release-name.documentId for release versions

Documents with dots in their IDs fall under the same access control rules as these internal documents for security reasons.

Bottom line: For your migration use case, I'd strongly recommend Option 2 (custom field) as it's more flexible, publicly accessible without authentication, and follows Sanity's best practices. You can still easily query and reference documents by their old IDs without the authentication complications.

We managed to resolve the situation by migrating again but this time use
prefix-old_id
curious why the client is behaving differently to the vision tool though (and is there some arbitrary check for a dot that caused this behaviour?).
By adding a prefix with a
.
, you’re creating a non-root path for that document. That means that they’re not considered public and require a token in order to be queried.
Ahhh so silly should have read up on that, thanks for the clarification
user M
!
It’s a super easy thing to miss!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?