Shopify GraphQL API Returns "Value can't be blank" When Trying to Delete Metafields with Empty Values

Problem

I’m having trouble removing metafields through Shopify’s GraphQL Admin API (2025-04). When I try to delete a metafield by setting its value to an empty string, I get an error saying the value cannot be blank. Based on what I read in Shopify docs, empty metafields should get removed automatically.

My Python Code

Here’s the mutation I’m using with the requests library:

if fields_to_remove:
    remove_mutation = """
    mutation RemoveMetafields($fields: [MetafieldsSetInput!]!) {
      metafieldsSet(metafields: $fields) {
        metafields {
          namespace
          key
          value
          createdAt
        }
        userErrors {
          field
          message
        }
      }
    }
    """

Request Data

The data I send looks like this:

{
  "fields": [
    {
      "ownerId": "gid://shopify/Product/1234567890",
      "namespace": "store",
      "key": "shipping_date",
      "value": "",
      "type": "single_line_text_field"
    },
    {
      "ownerId": "gid://shopify/Product/1234567890",
      "namespace": "store",
      "key": "sold_date",
      "value": "",
      "type": "single_line_text_field"
    }
  ]
}

Error Message

But I keep getting this error back:

[{
  "field": ["fields", "0", "value"],
  "message": "Value can't be blank."
}]

What I Want

I need these metafields to be completely removed so they don’t show up in my theme anymore. Is there a different way to do this? Maybe a special value I should use instead of an empty string? Or do I need to use a different GraphQL mutation entirely?

Any help would be great because I’m stuck on this issue.

The Problem

You’re trying to programmatically activate a Front Chat widget by clicking a custom button, but the click() event on the widget’s div element (with role="button") isn’t always working reliably. This is likely due to the chat widget loading asynchronously after the initial page load, leading to timing issues where your script attempts to interact with the widget before it’s fully rendered. Additionally, using .click() on a div may not always be the most reliable method.

TL;DR: The Quick Fix:

Use MutationObserver to wait for the chat widget to appear in the DOM before attempting to trigger it. Use dispatchEvent with a MouseEvent to trigger the widget more reliably.

:thinking: Understanding the “Why” (The Root Cause):

The core issue is the asynchronous loading of the Front Chat widget. Your script executes before the widget’s HTML is fully loaded and inserted into the DOM. Therefore, document.querySelector(".chat-widget") returns null, and the .click() method fails silently. Additionally, while a div with role="button" is semantically a button, relying solely on the .click() method for interaction can be unreliable across different browsers and widget implementations. dispatchEvent offers a more consistent and controlled way to simulate a click event.

:gear: Step-by-Step Guide:

Step 1: Wait for the Widget with MutationObserver

This approach uses a MutationObserver to monitor changes to the DOM. When the .chat-widget element appears, it triggers the activation. This ensures your script interacts with the widget only after it’s loaded:

const chatWidgetObserver = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    if (mutation.addedNodes.length > 0) {
      const chatWidget = document.querySelector(".chat-widget");
      if (chatWidget) {
        chatWidgetObserver.disconnect(); // Stop observing after the widget is found
        activateChatWidget(chatWidget);
      }
    }
  });
});

const chatWidgetConfig = { childList: true, subtree: true }; // Observe added nodes and their subtrees
chatWidgetObserver.observe(document.body, chatWidgetConfig); // Observe changes in the entire document body

function activateChatWidget(chatWidget) {
    //Step 2: Simulate a click with dispatchEvent
    const clickEvent = new MouseEvent('click', {
        'view': window,
        'bubbles': true,
        'cancelable': true
      });
      chatWidget.dispatchEvent(clickEvent);
}

Step 2: Attach the Event Listener to Your Custom Button

This step remains the same. The event listener calls the function to activate the chat widget after it has been observed in the DOM:

document.querySelector(".help-trigger").addEventListener("click", function () {
  //The activateChatWidget function is called here once the widget exists.  
});

Step 3 (Handling iframes):

If the chat widget is within an iframe, you need to access it through the iframe’s content window. First, get a reference to the iframe and then access the .chat-widget within its content document. This should be placed inside the activateChatWidget function from Step 1.

if(window.frames && window.frames.length > 0){ // Check if iframes exist
    for (let i = 0; i < window.frames.length; i++) {
      const iframe = window.frames[i];
      if (iframe.contentDocument.querySelector(".chat-widget")) {
        const chatWidget = iframe.contentDocument.querySelector(".chat-widget");
        const clickEvent = new MouseEvent('click', {
          'view': window,
          'bubbles': true,
          'cancelable': true
        });
        chatWidget.dispatchEvent(clickEvent);
        return; // Exit after finding and clicking in the iframe
      }
    }
    console.error('Chat widget not found within iframes.'); //handle cases where it is still not found.
  }

:mag: Common Pitfalls & What to Check Next:

  • Incorrect Selector: Double-check that .chat-widget and .help-trigger correctly target your elements. Use your browser’s developer tools to inspect the HTML structure.
  • Conflicting JavaScript: Other scripts on your page might interfere with the widget or event handling. Try disabling other scripts temporarily to isolate the problem.
  • Front Chat Specifics: Consult Front Chat’s documentation for any specific guidance on activating their widget programmatically. They might provide a dedicated API or event that’s more reliable than simulating a click.
  • Iframe Security: If your widget is in an iframe from a different domain, you might encounter cross-origin restrictions. This typically requires careful CORS configuration on the iframe’s origin server.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

Hit this exact issue last month during a product migration. Shopify won’t accept empty strings in metafieldsSet - it treats them as invalid input, not deletion requests. Here’s what actually works: use a two-step process. First, fetch the metafield IDs with a query like product(id: "your-product-id") { metafields(first: 250) { edges { node { id namespace key } } } } and filter for what you want gone. Then hit metafieldsDelete with those IDs. Way cleaner than fighting validation errors, and it actually purges the metafields from the database instead of leaving empty junk behind.

That’s actually expected behavior. Shopify’s metafieldsSet mutation won’t accept empty strings - it needs a valid value, which is why you’re getting that validation error. The documentation about automatic removal covers different scenarios.

You need to use metafieldsDelete instead. First, query to get the metafield IDs, then use those IDs in the delete mutation:

mutation DeleteMetafields($metafields: [ID!]!) {
  metafieldsDelete(metafields: $metafields) {
    deletedMetafields {
      id
    }
    userErrors {
      field
      message
    }
  }
}

Just pass an array of metafield IDs instead of trying to set empty values. This’ll cleanly remove the metafields from your products so they won’t show up in your theme templates.

The Problem

You’re trying to remove metafields from Shopify products using the GraphQL Admin API, but the metafieldsSet mutation returns a “Value can’t be blank” error when you attempt to delete metafields by setting their value to an empty string. You’ve correctly identified that the documentation suggests empty metafields should be automatically removed, but this isn’t how the API behaves.

:thinking: Understanding the “Why” (The Root Cause):

The metafieldsSet mutation is designed to update metafields, not delete them. Setting the value to an empty string is interpreted as an invalid update attempt, resulting in the error. Shopify’s GraphQL API requires a dedicated mutation for deleting metafields: metafieldsDelete. Attempting to work around this limitation by sending an empty string isn’t supported.

:gear: Step-by-Step Guide:

This solution uses a two-step process to efficiently remove metafields: fetching the IDs and then deleting them. It’s efficient and avoids the validation errors. If you have a large number of metafields, consider the alternatives discussed below.

Step 1: Fetch Metafield IDs

First, you need to retrieve the IDs of the metafields you want to delete. Use a GraphQL query like this:

query GetMetafields($productId: ID!) {
  product(id: $productId) {
    metafields(first: 250) {
      edges {
        node {
          id
          namespace
          key
        }
      }
    }
  }
}

Remember to replace "your-product-id" with the actual id of your product. The first: 250 argument limits the number of metafields returned; adjust as needed. This query returns an array of metafield objects, each with an id, namespace, and key. Filter this array in your Python code to select only the metafields you want to delete.

Step 2: Delete Metafields Using metafieldsDelete

Once you have the IDs, use the metafieldsDelete mutation:

mutation deleteMetafields($ids: [ID!]!) {
  metafieldsDelete(metafields: $ids) {
    deletedMetafields {
      id
    }
    userErrors {
      field
      message
    }
  }
}

Send a list of the metafield id values within the $ids variable. This mutation will remove the specified metafields.

Step 3 (Python Implementation):

Here’s an example of how to integrate the above GraphQL queries and mutations into your Python code using the requests library:

import requests
import json

# ... (Your GraphQL endpoint and headers) ...

def delete_metafields(product_id, metafield_keys_to_delete):
  # Step 1: Fetch Metafield IDs
  query = """
  query GetMetafields($productId: ID!) {
    product(id: $productId) {
      metafields(first: 250) {
        edges {
          node {
            id
            namespace
            key
          }
        }
      }
    }
  }
  """
  variables = {"productId": product_id}
  response = requests.post(graphql_endpoint, json={"query": query, "variables": variables}, headers=headers)
  data = response.json()
  metafields = data['data']['product']['metafields']['edges']
  ids_to_delete = [mf['node']['id'] for mf in metafields if mf['node']['key'] in metafield_keys_to_delete]


  #Step 2: Delete Metafields
  mutation = """
  mutation deleteMetafields($ids: [ID!]!) {
    metafieldsDelete(metafields: $ids) {
      deletedMetafields {
        id
      }
      userErrors {
        field
        message
      }
    }
  }
  """
  variables = {"ids": ids_to_delete}
  response = requests.post(graphql_endpoint, json={"query": mutation, "variables": variables}, headers=headers)
  print(response.json())

# Example Usage
product_id = "gid://shopify/Product/1234567890"  # Replace with your product ID
metafield_keys = ["shipping_date", "sold_date"]
delete_metafields(product_id, metafield_keys)

:mag: Common Pitfalls & What to Check Next:

  • Incorrect Product ID: Double-check that you’re using the correct product_id in your GraphQL queries.
  • API Rate Limits: If you’re processing many products, be mindful of Shopify’s API rate limits. Implement error handling and potentially batch your requests to avoid exceeding them.
  • Authentication: Ensure your API credentials are correctly configured in your headers.
  • Large-Scale Deletion: For large-scale metafield cleanup across numerous products, consider using Shopify’s Admin API with a more efficient approach or a third-party tool.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.