What is the best way to validate that e.g. a title for a document is unique within its type?

5 replies
Last updated: Sep 21, 2020
Hi,What is the best way to validate that e.g. a title for a document is unique within its type?
Is it to make a custom inputComponent for the fields and make a client groq check before updating the value, or is there a smarter, simpler way?
AI Update

The best way to validate that a title is unique across documents of the same type is to use a custom validation rule with Rule.custom() that queries your dataset using GROQ. This is the recommended approach rather than creating a custom input component.

Here's how to implement it:

defineField({
  name: 'title',
  type: 'string',
  validation: (rule) =>
    rule.custom(async (title, context) => {
      if (!title) return true; // Allow empty values (use .required() separately if needed)
      
      const { document, getClient } = context;
      const client = getClient({ apiVersion: '2024-01-01' });
      const id = document._id.replace(/^drafts\./, '');
      
      const query = `count(*[_type == $type && title == $title && !(_id in [$id, $draftId])])`;
      const params = {
        type: document._type,
        title,
        id,
        draftId: `drafts.${id}`,
      };
      
      const count = await client.fetch(query, params);
      
      return count === 0 ? true : 'This title is already in use';
    }),
})

Key points about this approach:

  1. Use getClient() from the validation context - This gives you access to the Sanity client within your validation rule to query the dataset, as explained in the Field Validation documentation.

  2. Exclude the current document - The query filters out both the published and draft versions of the current document using !(_id in [$id, $draftId]) to avoid false positives when editing existing documents.

  3. Handle draft IDs properly - Strip the drafts. prefix from the document ID to ensure proper comparison.

  4. Return appropriate values - Return true for valid values or an error message string for duplicates.

Performance consideration: Since this validation queries the dataset on every keystroke, you might want to debounce it or only run it on blur for better performance with large datasets. You can also use .warning() instead of .error() if you want to allow duplicates but warn editors about them:

validation: (rule) =>
  rule.custom(async (title, context) => {
    // ... same logic as above
  }).warning('This title is already in use')

Important note about validation scope: As mentioned in the validation documentation, validation rules currently operate client-side in the Studio only. API mutations bypass these validation rules. If you need to enforce uniqueness server-side (for API mutations or as an additional safeguard), you should implement additional checks using Sanity Functions with document-level event handlers.

This approach is simpler and more maintainable than building a custom input component, as it keeps your validation logic co-located with your schema definition where it belongs.

Show original thread
5 replies
Hi Louise, I think there’s no easy function available for this, unfortunately (unlike e.g.
slug
type fields that have an
isUnique()
option). However, I don’t think you’d have to create a custom input component to achieve it. Instead, you could use an async operation to check inside the validation function itself: https://www.sanity.io/docs/validation#custom-validation-091e10f957aa
Ok thanks
user M
- I have been looking at the examples previously. How do I validate against other documents though? I do not think they are available in the context parameter. Should I have a reference to a client and do a groq lookup?
Indeed, I would import the pre-configured client and check for existing docs with the title:
import client from 'part:@sanity/base/client'
. There might be an alternative way but it’s Monday and I can’t come up with it if so 😉 Will update if I think of something but in my mind this approach is the most straightforward.
Ok great, thanks.An example for others that might find this thread:

import client from 'part:@sanity/base/client'
...
{
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: Rule => Rule.required().custom((title) => {
        return client.fetch(`count(*[_type == "category" && title == "${title}"])`)
        .then(count => {
          if (count > 1){
            return 'Title needs to be unique'
          }else{
            return true
          }
        })
      })
    },
Thanks for sharing the outcome! 🙌

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?