How to fetch all nested references dynamically with one GROQ query?

7 replies
Last updated: Nov 30, 2025
Hi all.
I have a GROQ question combined with “how would you solve it” and “possible ways to solve it”.
Tried search but couldn’t find anything on it.

I’m looking for a way to get all references and all its data down the line with one GROQ query. The thing that makes it hard for me is that I have structured it in a quite dynamic way for the editor. So I’m never sure what to expect.

This query


_*[slug.current == "_${queryParameter}"_]_

gets me the page and its fields but the interesting part is this below, it’s a content area with different blocks. To break it down menu has its reference and that reference has it references and so on.


{
  "_key": "c12345",
  "_type": "uiHero",
  "image": {
    "_type": "image",
    "alt": "deer",
    "asset": {
      "_ref": "image-png",
      "_type": "reference"
    }
  },
  "size": "medium"
}{
  "_key": "abc",
  "_type": "uiMenu",
  "description": "Här har vi en lagom beskrivning för denna menyn",
  "menu": {
    "_ref": "<tel:1234-1234-1234|1234-1234-1234>",
    "_type": "reference"
  },
  "title": "Nya menyn"
}
other examples
• page (doucment) &gt; section (object) &gt; grid (object) &gt; information-card (object)
• page (document) &gt; section (object) &gt; menu (document) &gt; menu-category (document) &gt; menu-items (document) &gt; menu-tags (document)
Is it possible to get what I want with one dynamic query or do I need to re-think? How would you do it?

Please let me know if I need to complement with schema structures.

Thanks
🙂
AI Update

Your question touches on a common challenge with GROQ - dereferencing nested references when you have a dynamic content structure. The good news is that GROQ's dereference operator (->) can handle nested references, but you need to chain them explicitly.

Single Query with Nested Dereferencing

You can dereference nested references by chaining the -> operator at each level:

*[slug.current == $queryParameter][0] {
  ...,
  contentArea[] {
    ...,
    // Dereference first level
    menu->,
    // For deeper nesting, chain the operator
    menu-> {
      ...,
      menuCategory->,
      menuCategory-> {
        ...,
        menuItems[]-> {
          ...,
          menuTags[]->
        }
      }
    },
    // Handle image assets
    image {
      ...,
      asset->
    }
  }
}

The Dynamic Content Challenge

Since you mentioned your structure is "quite dynamic" with varying block types, you'll want to handle different _type values. Here's a more robust approach:

*[slug.current == $queryParameter][0] {
  ...,
  contentArea[] {
    ...,
    _type == "uiMenu" => {
      ...,
      menu-> {
        ...,
        categories[]-> {
          ...,
          items[]-> {
            ...,
            tags[]->
          }
        }
      }
    },
    _type == "uiHero" => {
      ...,
      image {
        ...,
        asset->
      }
    },
    // Add other block types as needed
  }
}

Important Limitations

GROQ doesn't support truly recursive or dynamic dereferencing - you need to know the depth and structure ahead of time. Each -> operator performs an internal subquery, so:

  1. Performance: Deep nesting with many references can be slow
  2. Explicit paths: You must specify each reference level explicitly
  3. No "dereference everything" wildcard: There's no way to say "follow all references to any depth"

Alternative Approaches

If your structure is too dynamic or too deeply nested, consider:

1. Multiple Queries (Client-side)

Fetch the initial page, then make targeted queries for specific references you discover. This gives you more flexibility but requires multiple round trips.

2. Restructure Your Schema

If you're hitting GROQ limitations, it might signal that your content model could be flattened. For example:

  • Store commonly-needed data directly on the parent instead of deeply nested references
  • Use arrays of inline objects instead of references where appropriate
  • Create "materialized views" that pre-flatten complex structures

3. Use Projections with select()

For truly dynamic content where you don't know what types to expect, you can use conditional projections:

contentArea[] {
  ...,
  select(
    defined(menu) => {
      "menu": menu-> {
        ...,
        categories[]->
      }
    },
    defined(image) => {
      "image": image.asset->
    }
  )
}

4. Handle Mixed Arrays with coalesce()

If you have arrays with both references and inline objects, you can handle them dynamically:

contentArea[]{
  ...,
  "resolvedContent": coalesce(
    @->,  // Resolve if it's a reference
    @     // Return as-is if it's already an object
  )
}

My Recommendation

For your use case, I'd suggest:

  1. Map out your actual content patterns - even if it feels "dynamic", you likely have 5-10 common patterns
  2. Write explicit dereference chains for each pattern (2-3 levels deep maximum)
  3. If you need deeper than 3 levels, consider whether that complexity is necessary or if you could restructure

The explicit approach may feel verbose, but it's more performant and maintainable than trying to create a "universal" query that handles infinite nesting. GROQ is designed for you to be explicit about what you're fetching, which helps with both performance and predictability.

Would love to see your schema structure if you want more specific advice on optimization!

Show original thread
7 replies

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?