Migrating nested block content: changing _type and adding fields in arrays
nameand sets a
fullname. But let's say for something more complicated
My document has a
bodyof block content and for example I want to look at all the blocks inside each document body and find the ones that are
_type == 'ctaButton'and change them to a new type
_type = 'newCtaButton'when I change it to the new type I also want to set some new values that didn't exist on the old type.
I see when I get the docs, I can filter down to all the
blocksinside the
bodythat I want to change, but then how do I create patches that will change the
typeof each of these blocks? It seems like maybe patches can only be performed on top-level documents and not child blocks - what's the best way to go about this?
doc.body.filter(block => block._type === 'ctaButton').map(block => ({
id: block._key,
patch: {
set: {}
}
}))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.
Show original thread5 replies
Was this answer helpful?
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.