Best way to model recursively nested documents in Sanity?

6 replies
Last updated: Dec 5, 2022
I’m sure this has been asked a bunch of times, but what’s the best solution for modelling a recursively nested document. So for example a “region” content type that could have any number of children or grandchildren to achieve a nested document structure like this:
UK => England => Oxfordshire
,
UK => Gibraltar
or
USA => Washington
AI Update

You're right, this is a common question! For your nested region structure (UK → England → Oxfordshire), the most straightforward approach is to use self-referencing documents with a parent reference field.

Here's how to model it:

export default defineType({
  name: 'region',
  type: 'document',
  fields: [
    {
      name: 'name',
      type: 'string',
      title: 'Region Name'
    },
    {
      name: 'parent',
      type: 'reference',
      title: 'Parent Region',
      to: [{type: 'region'}],
      // Optional: helps prevent circular references
      validation: Rule => Rule.custom((parent, context) => {
        if (parent?._ref === context.document?._id) {
          return 'A region cannot be its own parent'
        }
        return true
      })
    }
  ]
})

With this structure:

  • UK would have no parent (top-level)
  • England would reference UK as its parent
  • Oxfordshire would reference England as its parent
  • Gibraltar would reference UK as its parent
  • Washington would reference USA as its parent

Querying the hierarchy

To fetch a region with all its children, you can use GROQ:

*[_type == "region" && _id == $regionId][0]{
  _id,
  name,
  "children": *[_type == "region" && parent._ref == ^._id]{
    _id,
    name
  }
}

For the full tree from a root region, you'd need to query recursively or fetch all regions and build the tree client-side.

Alternative: Array of children

You could also model this top-down with an array of child references:

{
  name: 'children',
  type: 'array',
  of: [{type: 'reference', to: [{type: 'region'}]}]
}

However, the parent reference approach is generally better because:

  • The reference field appears where editors naturally work (on the child document)
  • It's easier to prevent orphaned documents
  • References are bidirectional when querying, so you can traverse both directions even though the UI only shows the field on the child

The parent reference approach scales well for unlimited nesting depth. Just be mindful that very deep hierarchies might require multiple queries or client-side tree building for full traversal. Sanity naturally supports these graph-like content structures through its reference system, making it flexible for complex hierarchical relationships.

Show original thread
6 replies
Would something like this be sufficient?

https://www.sanity.io/guides/parent-child-taxonomy
I have setup a parent reference field to the same document.
What’s the best way to query a recursively nested referenced documents?
That guide is how I’d do it!
If you’re querying with GROQ, you can use the
references()
function to find other documents that reference a given _id.
So you could query for the
England
document and all documents that have it as their parent reference children like this:
*[title == "England"][0]{
  title,
  "children": *[references(_id)]{
    title
  }
}
Thanks for the feedback Right now I am querying the the document tree in both directions like this:
{
  "topDown": *[_type == "region" && name == "Taiwan"][0] {
    name,
    "children": *[_type == "region" && references(^._id)][0] {
      name,
      "children": *[_type == "region" && references(^._id)][0] {
        name,
        "children": *[_type == "region" && references(^._id)][0] {
          name,
        },
      },
    },
  },
  "downUp": *[_type == "region" && name == "Xinyi"][0] {
    name,
    parent -> {
      name,
      parent -> {
        name,
        parent -> {
          name,
        },
      },
    },
  },
}
…and that’s getting overlay complex and each nested document level would get more complicated. I was wondering if there’s any nicer ways to do this kind of recursive querying.

It’s only going to be for generating URLs and breadcrumbs, so not super critical
I see!
There’s no way to do a recursive query like this, unfortunately. You need to dig down and resolve each one.

In this instance, do
topDown
and
downUp
return the same data, but in reverse order? If so, do you need both?
Also if you know explicitly which field will contain the parent reference, this query might be faster:
"children": *[_type == "region" && parent._ref == ^._id]

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?