Correct JSON Format for rich_text_field in Shopify GraphQL Mutation?

Topic summary

Issue: Developers are struggling to update Shopify product metafields of type rich_text_field using the GraphQL Admin API in Python. Multiple JSON formatting attempts have failed with validation errors:

  • Plain strings rejected as invalid JSON
  • Escaped strings fail schema validation (“not an object”)
  • Portable Text arrays also rejected

Shopify appears to expect a specific format, but documentation lacks clear examples.

Resolution: A working solution was provided using the productVariantsBulkUpdate mutation. Key implementation details:

  • The value field must be a stringified JSON object (not array)
  • Correct format: json.dumps({"type": "root", "children": [{"type": "paragraph", "children": [{"type": "text", "value": "text content"}]}]})
  • Use bulk operations for updating multiple variants efficiently
  • The solution includes complete Python code with proper headers, query structure, and error handling

Supporting materials: Working code example provided with comments, plus screenshots showing metafield settings and successful bulk operation results. The solution was discovered using Shopify’s AI Assistant in their GraphQL API documentation.

Summarized with AI on October 25. AI used: claude-sonnet-4-5-20250929.

I’m trying to update Shopify product metafields of type rich_text_field using the GraphQL Admin API in a Python script. However, I’m consistently running into issues with the value field formatting.

Here’s what I’ve tried so far:

  1. Plain string value: “value”: “Some value”
    Result: “Value is invalid JSON: unexpected token at ‘Some value’.”
  2. Escaped string as JSON: { “value”: “"Some value"” }
    Result: “Value is not the correct format: failed schema For ‘#’, "Some value" is not an object.”
  3. JSON array for Portable Text (as suggested by ChatGPT):“value”: “[{"type":"paragraph","children":[{"text":"A good product"}]}]”
    Result: “Value is not the correct format: failed schema For ‘#’, [{"type" => "paragraph", "children" => [{"text" => "A good product"}]}] is not an object.”

I’ve tried various encoding styles and payload structures, but none seem to be accepted. It looks like Shopify expects a specific format for rich_text_field values, but I can’t find a working example in the documentation or community.

:red_question_mark: Has anyone successfully updated a rich_text_field metafield using GraphQL?

  • What is the correct JSON structure for the value?

  • Does the value need to be a stringified JSON object or just raw JSON

Any guidance or working examples would be greatly appreciated!

Thanks in advance.

Accept as Solution

0

Report

Reply

I’m having the exact same problem and came to find the answer here. If I come across it I will send a further reply.

I am also trying to use the Shopify Admin GraphQL API with Python. I was able to find the correct solution using the AI Assistant in the Shopify GraphQL API documentation.

Below is the code, which includes comments. I have also included screenshots of the metafield settings and the bulk operation results.

import requests
import json


def fill_metafields_in_products_TEST():
    # * Variables
    # ACCESS_TOKEN = "shpat_****"
    # STORE_NAME = "test-dev-store-****"  # without .myshopify.com
    # GRAPHQL_VER = "2025-07"
    # GRAPHQL_ENDPOINT = (
    #     "https://"
    #     + STORE_NAME
    #     + ".myshopify.com/admin/api/"
    #     + GRAPHQL_VER
    #     + "/graphql.json"
    # )

    headers = {
        "Content-Type": "application/json",
        "X-Shopify-Access-Token": ACCESS_TOKEN,
    }

    # 1. Find ONLY ONE product, at least with 2 variants and query only first 5.
    # Also Querying metafields with namespace
    find_product_query = """
    {
    products(first: 1, query: "variants_count:>=2") {
        edges {
        node {
            id
            title
            variants(first: 5) {
            edges {
                node {
                id
                title
                sku
                metafields(first: 10, namespace: "custom") {
                    edges {
                    node {
                        id
                        key
                        value
                        namespace
                    }
                    }
                }
                }
            }
            }
        }
        }
    }
    }
    """

    response = requests.post(
        GRAPHQL_ENDPOINT, json={"query": find_product_query}, headers=headers
    )
    data = response.json()

    # Check, if product was found
    if not data.get("data") or not data["data"]["products"]["edges"]:
        print("[ERROR] No products found with two or more variants")
        return

    # 2. Process first product
    product = data["data"]["products"]["edges"][0]
    product_id = product["node"]["id"]
    product_title = product["node"]["title"]
    variants = product["node"]["variants"]["edges"]

    print(f"[DEBUG] Processing product with title: '{product_title}'")

    # * Preparing data for bulk update
    variants_data = []

    for variant in variants:
        variant_id = variant["node"]["id"]
        variant_title = variant["node"]["title"]

        # Check, if this variant's metafield is already filled
        existing_metafield_id = None
        if variant["node"]["metafields"]["edges"]:
            for metafield in variant["node"]["metafields"]["edges"]:
                # NOTE Queried metafields with our namespace, so it's combination of namespace+key
                if metafield["node"]["key"] == "variant_description":
                    existing_metafield_id = metafield["node"]["id"]
                    break

        # * Correct format for rich text field
        rich_text_value = {
            "type": "root",
            "children": [
                {
                    "type": "paragraph",
                    "children": [
                        {
                            "type": "text",
                            "value": f"Variant description: This is cool {variant_title}, description success!",
                            "bold": True,
                        },
                        {
                            "type": "text",
                            "value": "\n\nThis product variant description was automatically added.",
                        },
                        {
                            "type": "text",
                            "value": f"\n\nProduct title: {product_title}",
                        },
                        {
                            "type": "text",
                            "value": f"\n\nVariant title: {variant_title}",
                        },
                    ],
                }
            ],
        }

        # Making data for metafields
        metafields = []

        if existing_metafield_id:
            # If exist, only updating (without specifying type)
            metafields.append(
                {"id": existing_metafield_id, "value": json.dumps(rich_text_value)}
            )
        else:
            # Creating new metafield with correct type
            metafields.append(
                {
                    "namespace": "custom",
                    "key": "variant_description",
                    "value": json.dumps(rich_text_value),
                    "type": "rich_text_field",
                }
            )

        variants_data.append({"id": variant_id, "metafields": metafields})

    # 3. Performing bulk mutation
    bulk_mutation = """
    mutation productVariantsBulkUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
        productVariantsBulkUpdate(productId: $productId, variants: $variants) {
            product {
                id
                title
            }
            productVariants {
                id
                title
                metafields(first: 5) {
                    edges {
                        node {
                            id
                            namespace
                            key
                            value
                            type
                        }
                    }
                }
            }
            userErrors {
                field
                message
            }
        }
    }
    """

    variables = {"productId": product_id, "variants": variants_data}

    mutation_response = requests.post(
        GRAPHQL_ENDPOINT,
        json={"query": bulk_mutation, "variables": variables},
        headers=headers,
    )

    result = mutation_response.json()
    print(f"[DEBUG] bulk update result: {json.dumps(result, indent=2)}")

    # Processing result
    if "errors" in result:
        print(f"[ERROR] Error in bulk update: {result['errors']}")
        return

    if "data" in result:
        bulk_result = result["data"]["productVariantsBulkUpdate"]

        if bulk_result.get("userErrors"):
            print(f"[ERROR] Errors while bulk updating: {bulk_result['userErrors']}")
        else:
            print(f"✅ Product updated successfully: {bulk_result['product']['title']}")
            for variant in bulk_result["productVariants"]:
                print(f"   ✅ Variant: {variant['title']}")
                for metafield in variant["metafields"]["edges"]:
                    metafield_data = metafield["node"]
                    print(
                        f"      📝 Metafield: {metafield_data['namespace']}.{metafield_data['key']}"
                    )

    print("[DEBUG] Variants metafields processed via bulk API!")


if __name__ == "__main__":
    fill_metafields_in_products_TEST()

Metafield in settings: chrome-8-B7wt-Jlyi-I hosted at ImgBB — ImgBB
Result in the product variant: HGfye2-LBBL hosted at ImgBB — ImgBB