Sanity Pioneers: Get early access to betas, extra AI credits, and a direct line to the engineering team. Apply now

Filter null/empty entries in GROQ array subquery for page builder schema

5 repliesLast updated: Dec 2, 2025

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

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.

Show original thread
5 replies

Was this answer helpful?

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.

Related contributions