How to query nested arrays and filter by property in GROQ (prizes with affiliations)

9 replies
Last updated: Apr 20, 2023
I cannot figure out how to query based on a property of an item in an array in another array. Great that the GROQ arcade has a relevant dataset: https://groq.dev/d2463396-6d5e-4085-a32e-5e3b1bd33624
1. How to only retrieve people who have a prize which has an affiliation to the city of "Leiden" for example?
2. Let's imagine that many people have many prizes (could not find more than 1 prize at first sight), and many prizes have many affiliations. How to make sure that in my results, the people object only has relevant prize with relevant affiliations (filter out affiliations that are not from the city of Leiden, and filter out prizes that do not have the right affiliation at all)
AI Update

Great question! Filtering nested arrays in GROQ can be tricky at first, but once you understand the pattern, it becomes quite powerful. Let me walk you through both scenarios using the Nobel Prize dataset from GROQ Arcade.

1. Filtering documents based on nested array properties

To retrieve only people who have a prize with an affiliation to the city of "Leiden", you need to use the count() function because GROQ doesn't automatically "drill down" through nested arrays the way you might expect:

*[count(prizes[].affiliations[city == "Leiden"]) > 0]

This works because:

  • prizes[] flattens the prizes array
  • .affiliations[city == "Leiden"] filters affiliations within each prize
  • count() checks if any matching affiliations exist
  • The outer filter > 0 ensures we only get people with at least one match

Important: You can't write *[prizes[].affiliations[].city == "Leiden"] because the [] operators create arrays at each level, and you'd end up comparing an entire array to "Leiden" rather than individual values.

2. Filtering AND projecting nested data

For your second question—where you want to "clean up" the results to show only relevant prizes and affiliations—you need to apply the same filtering logic in your projection:

*[count(prizes[].affiliations[city == "Leiden"]) > 0] {
  _id,
  firstname,
  surname,
  "prizes": prizes[count(affiliations[city == "Leiden"]) > 0] {
    ...,
    "affiliations": affiliations[city == "Leiden"]
  }
}

This query does three things:

  1. Filters documents at the root level (only people with Leiden affiliations)
  2. Filters the prizes array to include only prizes that have Leiden affiliations
  3. Filters the affiliations array within each prize to show only Leiden affiliations

The result is a clean dataset where you can immediately see which specific prize and affiliation caused the match—super helpful when debugging complex filters or presenting filtered data to users.

The key pattern

The pattern here is: when filtering nested arrays, use count(array[condition]) > 0 at each level. This is covered in the GROQ pipeline components documentation, which explains how filters work with arrays.

This same technique applies to any nested structure—whether it's products with variants with pricing plans (as mentioned in the Sanity community discussion about this exact topic), or any other deeply nested content model in your Sanity dataset.

Show original thread
9 replies
Probably
user L
has some god level way to do it, but I would have done it like this:
*[count(prizes[count(affiliations[city == "Leiden"])>0])>0]

I need the people more than the count though. You got me thinking and I tried
*[prizes[].affiliations[].city == "Leiden"]
but it did not return any results.
I haven't looked at your original query yet, just wanted to point out that your last one will be comparing the array of cities, i.e. becomes
[..., ...,] == "Leiden"]
. The operators don't associate the way you think.
You would need to count here:

*[count(prizes[].affiliations[city == "Leiden"]) > 0]
I think the above answers your first question, at least.
Yes, getting somewhere, thanks! I just tried in Vision with in our studio, and it's unexpected (to me) that it returns results where some properties (like
pricingPlans
in this case) are NOT defined. As we can see on the right, the first item in the results does have a productVariants property with one item, but this one does not have any pricingPlan so it cannot have one with quantityPolicy set to "unlimited".
With this filter you do get the people tho? The count is just the filter used
Perfect, thanks
user A
. This is what I needed for 1. indeed! I don't have all the right reflex when writing GROQ queries yet.
Looks like I could resolve my 2nd point with this query:

*[
  _type == "productHead" &&
  count(productVariants[count(pricingPlans[quantityPolicy == "unlimited"]) > 0]) > 0
  
]{
  _id,
  internalName,
  "productVariants": productVariants[count(pricingPlans[quantityPolicy == "unlimited"]) > 0]{
    ...,
    "pricingPlans": pricingPlans[quantityPolicy == "unlimited"]
  }
}
With this request, my result only contains products which, deep down, have pricing plan with the quantity policy set to unlimited. But each product data is also "cleaned" of all pricing plans that are NOT unlimited and all variants that do not have any unlimited plans. I needed this projection because when a product has 10 variants, and each variant has 10 pricing plans it was too time consuming to find out WHY the GROQ filter returned this product. I wanted to quickly find "Which plan was matched by the filter?". Thanks both of you!
Great, I would also consider doing a more document based approach with this content model – separating variations and pricing plans into documents would make this quite easier I think
It may, but that ship has sailed a long time ago for our project. Also, "dangling" variants and pricing plans is not something that makes sense for us (we think). They would never be reused elsewhere (that's more or less how I decide to embed data in a document or to create a new document type which will be referenced).

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?