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

12 replies
Last updated: Aug 30, 2021
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
Hey User! Glad to hear you’re enjoying Sanity! To clarify, are you looking to make a field read-only after you’ve added the items you want to the array? Or are you looking to set up an Initial Value Template ?
Thanks for your answer basically I’d like the field to become read-only as in not being able to change the array items but still being able to modify the item content.Reading the Initial value template right now.
Got it: so you want to add the items to the array, then make it so that the objects cannot be added or removed, but can still have their content edited. Initial Value Templates are not what you’re looking for here, then. Let me think about how best to structure this!
Exactly ! And at the same time making it “invisible” for the content editor in the end (the fact that this field is an array) he would just see the array contents as fields.
Basically this would allow me to have a starter in which (in a simplified way) I can have a page document with a field Content which is an array of modules,
so i can use previously coded modules, maybe add a few more then create few pages document on the fly and then “lock it” .

So in the end the content editor just see page1 with module1 as content page2 with module2 as content and so on.
Exactly ! And at the same time making it “invisible” for the content editor in the end (the fact that this field is an array) he would just see the item.
Basically this would allow me to have a starter in which (in a simplified way) I can have a page document with a field Content which is an array of modules,
so i can use previously coded modules, maybe add a few more then create few pages document on the fly and then “lock it” .

So in the end the content editor just see page1 with module1 as content page2 with module2 as content and so on.
Thanks a lot for answering !
any thought
user M
? 🙂
I've played around with this, and unfortunately, this will be a pretty complex thing to tackle. Basically, you would have to build a document that uses the Document Actions API to generate an Initial Value Template on publish, based upon the modules you've added to the document. It's not impossible, but it will take some heavy lifting on your end.
I've played around with this, and unfortunately, this will be a pretty complex thing to tackle. Basic, you would have to build a document that uses the Document Actions API to generate an Initial Value Template on publish, based upon the modules you've added to the document. It's not impossible, but it will take some heavy lifting on your end.
Thanks a lot for the insights, I’m glad to head you didn’t find a solution in minutes since I’ve spent quite some time working on that already 🙂.Support is awesome as usual !
Thanks Sanity Team !
And I did manage to find a solution for the time being with conditional fields, release came on point 🙂 !
Awesome! Conditional fields coming in handy already!

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?