Dynamic validation for optional object fields in Sanity.io

4 replies
Last updated: Oct 29, 2021
I've got an object with a bunch of fields with validation. The object is optional (using the
hidden()
function). Is there a way to only validate fields if the object they're part of is not hidden?
AI Update

Yes, you can conditionally validate fields based on whether their parent object is hidden! The key is to use Sanity's custom() validation method and check the parent object's visibility condition within your validation logic.

Here's how to approach this:

Solution: Conditional Validation Based on Parent Visibility

Since validation runs independently of the hidden() function, you need to replicate the same condition in your field validation. Here's a practical example:

{
  name: 'myObject',
  type: 'object',
  hidden: ({document}) => !document?.showAdvancedOptions, // Object is conditionally hidden
  fields: [
    {
      name: 'requiredField',
      type: 'string',
      validation: (rule) => 
        rule.custom((value, context) => {
          // Only validate if parent object would be visible
          const isParentVisible = context.document?.showAdvancedOptions;
          
          if (!isParentVisible) {
            return true; // Skip validation when parent is hidden
          }
          
          // Validate normally when parent is visible
          return value ? true : 'This field is required';
        })
    }
  ]
}

Important Considerations

As noted in the conditional fields documentation, hidden fields that are marked as required will still enforce validation, even when hidden. This is a known behavior in Sanity Studio, which is why you need the conditional validation approach above.

Pattern for Complex Scenarios

If your object has multiple fields with validation, you might want to extract the visibility logic into a helper function to keep things DRY:

const isObjectVisible = (context) => {
  return context.document?.showAdvancedOptions === true;
};

{
  name: 'myObject',
  type: 'object',
  hidden: ({document}) => !isObjectVisible({document}),
  fields: [
    {
      name: 'field1',
      type: 'string',
      validation: (rule) => 
        rule.custom((value, context) => 
          !isObjectVisible(context) || value 
            ? true 
            : 'Required when object is visible'
        )
    },
    {
      name: 'field2',
      type: 'string',
      validation: (rule) => 
        rule.custom((value, context) => 
          !isObjectVisible(context) || value 
            ? true 
            : 'Required when object is visible'
        )
    }
  ]
}

This approach ensures your validation logic stays synchronized with your visibility conditions, and fields are only validated when their parent object is actually visible to the editor.

Ah right, but I don't know what the conditions will be, because this object can be used in a lot of different documents (it's a link type object, with label, internal en external field). Any way to get the actual hidden value used for the parent?
What does your object schema look like? I'll play around with it to see if I can make the validation rule dynamic based off of an unknown parent's visibility.
Cool, this is the
link
object:

export const link = {
  title: 'Link',
  name: 'link',
  type: 'object',
  fields: [
    {
      title: 'Type',
      name: 'type',
      type: 'string',
      initialValue: 'internal',
      options: {
        layout: 'radio',
        direction: 'horizontal',
        list: [
          { value: 'internal', title: `Internal` },
          { value: 'external', title: `External` }
        ]
      }
    },
    {
      title: 'Tekst',
      name: 'text',
      type: 'string',
      validation: Rule => Rule.required().error('This field is required')
    },
    {
      title: 'Url',
      name: 'internalLink',
      type: 'internalLink',
      hidden: ({ parent }) => parent?.type !== 'internal',
      validation: requiredIf(({ parent }) => parent?.type === 'internal')
    },
    {
      title: 'Url',
      name: 'externalLink',
      type: 'externalLink',
      hidden: ({ parent }) => parent?.type !== 'external',
      validation: requiredIf(({ parent }) => parent?.type === 'external')
    }
  ]
}

function requiredIf(fn) {
  return Rule => Rule.custom((value, context) => (!value && fn(context)) ? 'This field is required' : true).error()
}
It's used another object, chapter:


export const chapter = {
  title: 'Hoofdstuk',
  name: 'chapter',
  type: 'object',
  fields: [
    {
      title: 'Type',
      name: 'chapterType',
      type: 'string',
      options: {
        list: [
          { title: 'Afbeelding', value: 'image' },
          { title: 'Afbeelding met tekst', value: 'imageWithText' },
          { title: 'Video', value: 'video' },
          { title: 'Geanimeerde quote', value: 'animatedQuote' },
          { title: '360 Afbeelding', value: '360' },
          { title: 'Link', value: 'link' },
        ]
      },
      validation: Rule => Rule.required().error('Dit veld is verplicht')
    },
    {
      title: 'Link',
      name: 'link',
      type: 'link',
      hidden: ({ parent }) => !['link'].includes(parent?.chapterType),
      validation: Rule => Rule.custom((value, context) => (
        !value && context.parent?.chapterType === 'chapterWithLink'
          ? 'Dit veld is verplicht' 
          : true
      ).error()
    }
  ]
}
Not really possible, right?

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?