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

12 replies
Last 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:

  1. Define both the array field AND individual module fields in your schema
  2. Use a boolean flag to track whether the page is "finalized"
  3. Use conditional hidden properties to swap which fields are visible
  4. Optionally use readOnly to prevent editing of finalized fields

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

  1. Static schema: You must define all possible module fields in your schema upfront. You can't dynamically create fields based on what's in the array.

  2. Field naming: The _type of your array items should match the field names (e.g., fooModule type → fooModule field).

  3. One-way transformation: Once finalized, you'd need to manually "unfinalize" by toggling the boolean and potentially recreating the array.

  4. Validation: Consider adding validation to ensure required modules are present before allowing finalization.

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

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?