Discussion on how to change nested block content in Sanity documents during data migration.
You're absolutely right that patches can only be performed on top-level documents, not directly on nested child blocks. However, you can still modify nested blocks within a document using the patch API's path syntax with JSONMatch.
The Solution: Use JSONMatch Path Syntax in Your Patches
When you patch a document, you target specific blocks within the body array using path expressions with the block's _key. Here's how to change the _type of nested blocks:
// For each block you want to change
const patches = doc.body
.filter(block => block._type === 'ctaButton')
.map(block => ({
id: doc._id, // Patch the DOCUMENT, not the block
patch: {
// Use the block's _key to target it specifically
set: {
[`body[_key=="${block._key}"]._type`]: 'newCtaButton',
[`body[_key=="${block._key}"].newField`]: 'new value',
// Add other new fields as needed
}
}
}))Complete Migration Example
Here's a more complete example using the Sanity client:
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'your-project-id',
dataset: 'your-dataset',
token: 'your-token',
apiVersion: '2025-05-16',
useCdn: false
})
async function migrateCtaButtons() {
// Fetch all documents with ctaButton blocks
const docs = await client.fetch(`
*[_type == "yourDocType" && body[]._type == "ctaButton"]
`)
for (const doc of docs) {
const blocksToUpdate = doc.body.filter(block => block._type === 'ctaButton')
// Build a single patch for all blocks in this document
const setOperations = {}
blocksToUpdate.forEach(block => {
const path = `body[_key=="${block._key}"]`
setOperations[`${path}._type`] = 'newCtaButton'
setOperations[`${path}.newField`] = 'default value'
// Add any other new fields you need
})
// Apply the patch
await client
.patch(doc._id)
.set(setOperations)
.commit()
console.log(`Updated ${blocksToUpdate.length} blocks in ${doc._id}`)
}
}
migrateCtaButtons()Key Points
Path Syntax: Use
body[_key=="abc-123"]._typeto target specific blocks by their_key. As noted in the patches documentation, the array filter must use double quotes, which need to be escaped in JSON.Single Patch: You can update multiple blocks in one document with a single patch operation by building up the
setobject with multiple paths.Document ID: Always patch using the document's
_id, never the block's_key. The_keyis only used within the path expression.Nested Fields: You can set deeply nested properties using dot notation:
body[_key=="..."].nested.field
Alternative: Using insert with replace
If you need to completely replace blocks (not just change fields), you can use the insert operation with replace:
const blockIndex = doc.body.findIndex(block => block._key === targetKey)
await client
.patch(doc._id)
.insert('replace', `body[${blockIndex}]`, [{
_key: targetKey, // Keep the same key
_type: 'newCtaButton',
// ... all new fields
}])
.commit()The JSONMatch syntax documentation has more details on path expressions, and the content migration cheat sheet provides additional migration patterns for complex scenarios like this.
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.