Discussion on how to change nested block content in Sanity documents during data migration.

5 replies
Last updated: Apr 26, 2020
Question about migrating data - this example is good (https://www.sanity.io/docs/migrating-data ). It unsets a document's
name
and sets a
fullname
. But let's say for something more complicated
My document has a
body
of 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
blocks
inside the
body
that I want to change, but then how do I create patches that will change the
type
of 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: {}
  }
}))
AI Update

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

  1. Path Syntax: Use body[_key=="abc-123"]._type to 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.

  2. Single Patch: You can update multiple blocks in one document with a single patch operation by building up the set object with multiple paths.

  3. Document ID: Always patch using the document's _id, never the block's _key. The _key is only used within the path expression.

  4. 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.

I believe you can use this script: https://github.com/sanity-io/sanity-recipes/blob/master/snippets/renameField.js
Edit the query:

client.fetch(`*['ctaButton' in body[]._type][0...100] {_id, _rev, name}`)
and edit the patch

{
    id: doc._id,
    patch: {
      set: {
        'body[_type=="ctaButton"]._type': "newCtaButton"
      },
      // this will cause the migration to fail if any of the documents has been
      // modified since it was fetched.
      ifRevisionID: doc._rev
    }
  }

For future reference, this library might be interesting https://www.npmjs.com/package/sanity-diff-patch
I believe you can use this script: https://github.com/sanity-io/sanity-recipes/blob/master/snippets/renameField.js
Edit the query:

client.fetch(`*['ctaButton' in body[]._type][0...100] {_id, _rev, name}`)
and edit the patch

{
    id: doc._id,
    patch: {
      set: {
        'body[_type=="ctaButton"]._type': "newCtaButton"
      },
      // this will cause the migration to fail if any of the documents has been
      // modified since it was fetched.
      ifRevisionID: doc._rev
    }
  }

For future reference, this library might be interesting https://www.npmjs.com/package/sanity-diff-patch
Interesting! Will try that

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?