Filtering out null/empty entries in GROQ subqueries for array fields in Sanity.io

5 replies
Last updated: Mar 19, 2022
Hi, general GROQ filter/subquery question… Is it possible to subquery an array field to filter out null/empty entries? For example, if you had a page builder schema with an array of different objects (text, image, video etc…) with a schema looking like this:

export default {
	name: 'page',
	title: 'Page',
	type: 'document',
	fields: [
		{
			name: 'content',
			type:'array', 
			of: 
			[
				{ type: 'page-block-hero' },
				{ type: 'page-block-text' },
				{ type: 'page-block-image' },
				{ type: 'page-block-video' },
				{ type: 'page-block-cta' },
				{ type: 'page-block-carousel' }
			]
		}
	]
}
I have a GROQ query that currently looks like this:


*[_type == "page" && slug.current == $slug]
{
		'content': content[]
		{
			'hero': hero->
			{
				heading,
				subheading
			},

			'image': image->
			{
				'src': file.asset->url,
				caption
			}
			...
		}
}
Trying to figure out how to filter each content type as to null out ones that don’t have any content…


*[_type == "page" && slug.current == $slug]
{
		'content': content[]
		{
			'hero': *[hero.heading != null]
			{
				heading,
				subheading
			},

			'image': *[image.file != null]->
			{
				'src': file.asset->url,
				caption
			}
			...
		}
}
Wondering A) is this possible with GROQ and B) What would a query look like to accomplish this…

Thanks!
AI Update

Yes, this is definitely possible with GROQ! The key is understanding that you need to filter at the right level and use the correct syntax. Let me break down the approaches:

The Core Issue

Your attempted syntax 'hero': *[hero.heading != null] won't work because you can't use the *[filter] syntax inside a projection like that. The * operator is specifically for fetching documents from the dataset, not for filtering within projections.

The cleanest approach is to use object-level conditional syntax with the format condition => { }. This only includes the object when the condition is true:

*[_type == "page" && slug.current == $slug] {
  'content': content[] {
    _type,
    defined(hero->heading) => {
      'hero': hero-> {
        heading,
        subheading
      }
    },
    defined(image->file) => {
      'image': image-> {
        'src': file.asset->url,
        caption
      }
    }
  }
}

The condition => { } syntax expands the object into the projection only when the condition evaluates to true. If false, nothing is added.

Solution 2: Using select() for Field-Level Logic

The select() function works like a switch statement and is great for field-level conditionals:

*[_type == "page" && slug.current == $slug] {
  'content': content[] {
    _type,
    'hero': select(
      defined(hero->heading) => hero-> {
        heading,
        subheading
      }
    ),
    'image': select(
      defined(image->file) => image-> {
        'src': file.asset->url,
        caption
      }
    )
  }
}

With select(), if no condition matches and there's no fallback value, it returns null. The syntax is select(condition => value, condition2 => value2, fallback).

Solution 3: Filter the Array Before Projecting

If you want to completely remove array items that don't have the required content (rather than just omitting fields), filter at the array level:

*[_type == "page" && slug.current == $slug] {
  'content': content[
    _type == "page-block-hero" && defined(hero->heading) ||
    _type == "page-block-image" && defined(image->file)
    // add conditions for other block types
  ] {
    _type,
    'hero': hero-> {
      heading,
      subheading
    },
    'image': image-> {
      'src': file.asset->url,
      caption
    }
  }
}

This filters the content array itself before projection, keeping only items that meet your criteria.

Key Functions

  • defined() - Returns true if a property exists and is not null
  • select() - Evaluates conditions in order and returns the first matching value
  • Conditional projection (condition => {}) - Expands objects into projections only when conditions are met

My recommendation: Use the object-level conditional approach (Solution 1) for your page builder use case. It's the most readable and maintainable, especially when different block types have different required fields.

There is some documentation on conditionals at this spot in the reference: https://www.sanity.io/docs/query-cheat-sheet#64a36d80be73
There are also two functions that might be relevant as far as what to base the conditions on -- and this is based on my understanding and a little experience -- a
defined( ) function that catches when something exists and coalesce( ) where you can cast a wide net out to things that may be non-null and the first one in your list that's valid will be what gets returned.
Thanks
user S
! Will take a look
Completely missed the conditionals block of the cheatsheet… looks very promising
user E
It's the only way I could make some of the things I wanted possible. And yeah, there are quite a many pages in the docs, but the full power of Sanity is pretty exhaustive. So I have gone through all the pages but people still bring stuff up and I am like how did I miss that?
I am just another user, but I have thought about making a guide for things like Desk Structure because it is a little abstract without examples (especially with such similar naming conventions), just taking one kind of task and applying the same kinds of things to it so people can see how it works in context. Lemme know how it turns out for you!
Yes agreed! This is my first project with Sanity and I have been continually impressed with how powerful GROQ is compared to GraphQL. I think that’s a great idea having a sanity Cookbook of sorts for common/useful tasks and customizations — I’ve been thinking the same thing as I figure various pieces out…

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?