How to lock array items and convert them to fixed fields in Sanity Studio?

12 repliesLast updated: Dec 1, 2025

Hello,5th project on Sanity, loving the tool and the possibilities.
With the experience for the previous projects I now have a pretty good idea on how I like to setup my schemas and I’m trying to build a starter for our next projects.

Worked hard yersterday but didn’t manage to build what I want :
Is it possible to have a field which is an array when I’m building the content in the studio then “locking those array items” ultimately replacing in the studio the field with just the content of the array.

For instance I have my page document which has an array of modules that I can pick when creating the page then after I’m done setting it up, the user has a Home page with say Foo module (in that array), but just sees the home page which has a field foo module.

Sorry not native english speaker, hope what I’m aiming to do is understandable.
AI Update

I understand what you're trying to achieve! You want editors to initially build a page using a flexible array of modules, but then "lock" that structure so they see individual module fields instead of an array interface. This is a creative approach to combining flexibility during setup with a cleaner editing experience afterward.

The good news is you can accomplish this using conditional fields with the hidden property. However, I need to clarify the architectural approach—Sanity schemas are static, so we can't dynamically create new fields at runtime. Instead, you'll need to define all potential module fields upfront and conditionally show/hide them.

The Approach

The strategy is to:

Implementation

Step 1: Schema with conditional visibility

export default {
  name: 'page',
  type: 'document',
  fields: [
    {
      name: 'title',
      type: 'string'
    },
    {
      name: 'isFinalized',
      type: 'boolean',
      title: 'Structure Finalized',
      description: 'Lock the page structure',
      initialValue: false
    },
    // Array shown during initial setup
    {
      name: 'moduleSetup',
      title: 'Page Modules (Setup)',
      type: 'array',
      of: [
        {type: 'fooModule'},
        {type: 'barModule'},
        {type: 'heroModule'}
      ],
      hidden: ({document}) => document?.isFinalized === true
    },
    // Individual fields shown after finalization
    {
      name: 'fooModule',
      type: 'fooModule',
      hidden: ({document}) => document?.isFinalized !== true,
      readOnly: ({document}) => document?.isFinalized === true
    },
    {
      name: 'barModule',
      type: 'barModule',
      hidden: ({document}) => document?.isFinalized !== true,
      readOnly: ({document}) => document?.isFinalized === true
    },
    {
      name: 'heroModule',
      type: 'heroModule',
      hidden: ({document}) => document?.isFinalized !== true,
      readOnly: ({document}) => document?.isFinalized === true
    }
  ]
}

Step 2: Create a migration script to transform data

When the editor toggles isFinalized to true, you'll need to manually copy data from the array into the individual fields. You can do this with a migration script:

// migrations/finalizePageStructure.js
import {getCliClient} from 'sanity/cli'

const client = getCliClient()

const query = `*[_type == "page" && isFinalized == true && defined(moduleSetup)]`

client.fetch(query).then(pages => {
  const patches = pages.map(page => {
    const operations = []
    
    // Copy each array item to its corresponding field
    page.moduleSetup?.forEach(module => {
      const fieldName = module._type
      operations.push({set: {[fieldName]: module}})
    })
    
    // Remove the setup array
    operations.push({unset: ['moduleSetup']})
    
    return client.patch(page._id).operations(operations).commit()
  })
  
  return Promise.all(patches)
})

Run this migration after editors mark pages as finalized.

Alternative: Custom Document Action

If you want a more automated approach, you can create a custom document action that performs the transformation when clicked:

// documentActions.js
import {useDocumentOperation} from 'sanity'
import {useCallback} from 'react'

function FinalizePageAction(props) {
  const {patch, publish} = useDocumentOperation(props.id, props.type)

  const onHandle = useCallback(() => {
    const modules = props.draft?.moduleSetup || []
    
    if (modules.length === 0) return
    
    // Build patch operations
    const patchOperations = []
    modules.forEach(module => {
      patchOperations.push({set: {[module._type]: module}})
    })
    
    // Execute patch
    patch.execute([
      ...patchOperations,
      {set: {isFinalized: true}},
      {unset: ['moduleSetup']}
    ])
    
    if (props.draft && !props.published) {
      publish.execute()
    }
  }, [props.draft, patch, publish])

  return {
    label: 'Finalize Structure',
    onHandle,
    disabled: props.published?.isFinalized || !props.draft?.moduleSetup?.length
  }
}

export default function resolveDocumentActions(props, context) {
  if (props.type === 'page') {
    return [...context.defaultResolve(props), FinalizePageAction]
  }
  return context.defaultResolve(props)
}

Register it in sanity.config.js:

import {defineConfig} from 'sanity'
import resolveDocumentActions from './documentActions'

export default defineConfig({
  // ... other config
  document: {
    actions: resolveDocumentActions
  }
})

Important Considerations

This approach gives you the flexibility you want during setup while providing a cleaner interface afterward. Your English was perfectly clear, and this is a smart pattern for a starter template! Good luck with your 5th project!

Show original thread
12 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.

Related contributions