Discussion on GROQ behavior and implementation choices in Sanity.io API

35 replies
Last updated: Mar 31, 2022
every version other than v1 which doesn’t return the key at all.
Mar 31, 2022, 2:05 PM
user H
, I don't believe this is a bug actually! In some ways, its probably more of an implementation thing for GROQ. When reading the documentation under the Filters Section , you notice that a key will be null when it does not exist. Thus, any version but v1 is returning null simply because the key does not exist, not because of an empty string or anything like that.
Mar 31, 2022, 2:08 PM
My guess would be that the implementation of the input component for strings will remove the value on an empty string. This in some ways is more consistent: falsey values will always unset the key rather than giving a default empty value like "" or [] or {} or 0.
Mar 31, 2022, 2:09 PM
But the key exists .. as if I use type->{_id, name} I get the _id and NULL for name .. so perhaps it is how GROQ (mis)handles the type. But if the field is empty I expect a string type definition to return ‘’, not NULL.
Mar 31, 2022, 2:12 PM
The query that was problematic was type->name return null.
Mar 31, 2022, 2:12 PM
You are defining the key through your projection. That's why it is null!
Mar 31, 2022, 2:13 PM
Try doing type-> and my guess would be that name would not appear at all
Mar 31, 2022, 2:13 PM
This then produced an update problem, even after the key was fixed. It was still returning null
Mar 31, 2022, 2:13 PM
As you said above, removing falsey values was the behaviour of API V1, but in subsequent versions it returns NULL on falsey or empty.
Mar 31, 2022, 2:17 PM
I guess we can get religious quickly on the topic of whether an empty string is falsey. 🙂
Mar 31, 2022, 2:18 PM
The way GROQ works is all about projections. Here are examples that may help:
Here is a projection that asks for all defined fields on a GROQ query for all movies

*[_type == 'movie']{...}
Let's say I don't want all of them, I just want the name of the movie and its rating.

*[_type == 'movie']{name, rating}
Great, that's solved. However, rating is not a required field, so it sometimes is not set. What should GROQ do? If rating is a string, it could give an empty string. Or maybe if rating is a number, it could be a -1. Based on the type and what you want, there would be all sorts of defaults. To get around this, GROQ has decided that it will unset the property if it is not set.

BUT, and here's the key, your query has asked to return the rating. So GROQ wants to honor this request, and has decided to return null on all unset values.
Mar 31, 2022, 2:18 PM
However, if you don't like this kind of behavior, there is a solution in the form of the

*[_type == 'movie']{name, 'rating': coalesce(rating, '')}
Mar 31, 2022, 2:20 PM
OK, I see. I understand the choices made here.
Mar 31, 2022, 2:21 PM
What I observed is that even after this user error was fixed, The API continued to return NULL. We are reviewing the logs to understand why the relationship was broken
Mar 31, 2022, 2:22 PM
Glad I could help! You are right though: v1 more closely represents your actual data. All other versions add this functionality on top of it which can be slightly confusing without knowledge of the reasons behind it.
Mar 31, 2022, 2:23 PM
And even more frustrating was that the vision plugin was returning the result correctly, even as calls from the production stack was getting back the wrong value. (I did ensure that I wasn’t talking to the CDN API, even though this would have healed up on subsequent publishes)
Mar 31, 2022, 2:24 PM
Ooh, so you were getting an empty string on the vision plugin but in production it was null? That is frustrating!
Mar 31, 2022, 2:25 PM
actually not an empty string .. but I was getting the correct string value back on the vision call, but on the stack API call, the exact same query, I was getting NULL. Yep.
Mar 31, 2022, 2:26 PM
So, my solution, sharing this naïvely, in case others have this edge case..
Mar 31, 2022, 2:26 PM
I am using webhooks to notify my production stack of updates in Sanity. As the webhook has a (unknown) payload limit, I have to call back to the Sanity API to get the update payload. On this call, it was returning NULL.So, I added the specific GROQ projection into the Webhook query triggered on UPDATE. When this projection was in that query. The API returned the key correctly. I can’t think of why.
Mar 31, 2022, 2:28 PM
And this whole case came about because a user cleared the string field in a type definition document linked to the requested document.
Mar 31, 2022, 2:30 PM
user G
- thank you!!If I’m following correctly, this may be the difference between a string type field that exists but is empty and a string type field that doesn’t exist in the document? If I delete a native string type field’s contents, the field itself is removed from the query unless I’ve done something like an empty initialValue or alter this with a custom component.

  "_createdAt": "2022-03-02T22:54:33Z",
  "_id": "5b5333e7-5fdf-4b28-81df-b9d0553d9a39",
  "_rev": "0stphb-jyu-jjg-kg1-mdjdh5f7b",
  "_type": "tag",
  "_updatedAt": "2022-03-30T03:58:52Z",
  "icon": "B", // <-- string
  "title": "badger"
// icon field emptied, item is removed
  "_createdAt": "2022-03-02T22:54:33Z",
  "_id": "5b5333e7-5fdf-4b28-81df-b9d0553d9a39",
  "_rev": "5k4d2t-snm-s0d-jp3-hb3g2tmuc",
  "_type": "tag",
  "_updatedAt": "2022-03-31T18:35:52.753Z",
// no "icon"
  "title": "badger"

Mar 31, 2022, 6:39 PM
user U
Thanks for your comment. It feels arbitrary to remove a known typed field from a response. We are compensating for this design choice. I agree that it is an improvement to return a NULL instead of V1's choice to unset the property.
Mar 31, 2022, 8:15 PM
And just for clarity. We are talking about a reference to an object definition and not a self contained property. The reference object has
{_id: "ID", name: "" }
Mar 31, 2022, 8:18 PM
so if I GROQ to that with
Mar 31, 2022, 8:19 PM
*[]{_id, type->{}}
Mar 31, 2022, 8:19 PM
It may be possible to do this by altering handlechange in a custom string component:
import React from 'react'
import { FormField } from '@sanity/base/components'
import { TextInput } from '@sanity/ui'
import PatchEvent, { set, unset } from '@sanity/form-builder/PatchEvent'

const MyStringInput = React.forwardRef((props, ref) => {
  const {
  } = props

  const handleChange = React.useCallback(
    (event) => {
      const inputValue = event.currentTarget.value
      // if new input value, add input value, else don't...
      onChange(PatchEvent.from(inputValue ? set(inputValue) : unset()))

  return (
        // add onChange to input

export default MyStringInput

Mar 31, 2022, 8:21 PM
I’m not certain off the top of my head if an empty string or other fn could be placed in this onChange ternary, but it might be worth a shot
onChange(PatchEvent.from(inputValue ? set(inputValue) : unset()))
Mar 31, 2022, 8:24 PM
Wow, this is super nice of you. Thank you. The better point for our team on this was to restrict the UI more and not let low level users into some of the definition objects. This is what I have turn my attention to.
Mar 31, 2022, 8:24 PM
This is pasted from my personal notes, please forgive any typos.
Mar 31, 2022, 8:24 PM
The API inconsistencies that followed were really hard to understand. The design choices for GROQ, once I could understand them. I can design around the expected behaviour.
Mar 31, 2022, 8:25 PM
But the weird stickiness of this projection on the API until I added the projection to the webhook query is still a mystery to me.
Mar 31, 2022, 8:26 PM
A strictly native option if you want to ensure this isn’t empty might be to add validation .
Mar 31, 2022, 8:26 PM
For building things and making sure traffic is minimized, I’m enjoying groq quite a bit. GraphQL is another fantastic and supported option. That unset appears to be part of the native input options though - which should be capable of being modified.
Mar 31, 2022, 8:28 PM
I am still very much in learning mode myself, and if you need anything further I’d be happy to run things past the team as well.
Mar 31, 2022, 8:29 PM
Yeah, my experience is with Neo4J, but I am warming to GROQ and modelling an admin in Studio is a HUGE time win. Also, the general speed of update is super impressive. So when something unexpected happens, you’re really not expecting it. 🙂
Mar 31, 2022, 8:33 PM

Sanity– build remarkable experiences at scale

The Sanity Composable Content Cloud is the headless CMS that treats content as data to power your digital business. Free to get started, and pay-as-you-go on all plans.

Was this answer helpful?