Project portfolio Browse selected work

Shopify Plus Upgrade Monthly Fee Reduction + Up to $4800 Development Fee Credit - Exclusive WesWoo Offer

Guide

Shopify GraphQL Response Compatibility: Validate Data Before Promotion

Published: Editorial review: 2026-10-04

A GraphQL client can receive HTTP 200 and still lack a usable product snapshot. This guide gives developers and release reviewers a small acceptance gate for a single-product response: verify the executed API version, distinguish missing data from a nullable result, validate selected fields, and stop when a status mapping becomes incompatible.

The references were checked on 4 October 2026. The example targets GraphQL Admin API 2026-10, an installed and authorized app with read_products, and a product ID from an authorized development store. Product IDs and responses below are synthetic. The Python gate was tested offline; no merchant API request was made. Availability follows API and authorization requirements rather than a plan label. API versioning, Product access requirements.

Define the response you intend to consume

Keep the query, variable policy, API version, and consumer field list together in version control. This query assigns a stable response key through the catalogItem alias:

query ProductContract($id: ID!) {
  catalogItem: product(id: $id) {
    id
    title
    status
    updatedAt
  }
}

For an offline fixture, use {"id":"gid://shopify/Product/1001"}. A real execution needs a product ID from the authorized store. The required ID! argument does not make the result non-null: the query returns nullable Product. 2026-10 product query.

To inspect the schema with your app's scopes, start the app's development session with shopify app dev and press g to open its GraphiQL instance. Inspect the selected fields in Docs Explorer before running an authorized read against the development store. A separately installed GraphiQL app has its own scopes and cannot prove that your app has the same access. Official GraphiQL workflow.

Raw requests use POST https://{store}.myshopify.com/admin/api/2026-10/graphql.json with X-Shopify-Access-Token. When using an official app template or client, follow its authentication flow. Keep credentials out of browser-facing examples and acceptance logs. Authentication and endpoint reference.

Separate API guarantees from consumer policy

The schema defines types, while the consumer decides what is sufficient for its own snapshot. A non-null marker, !, prevents null; it does not promise a non-empty title. A rule requiring a non-blank title belongs in a separate business check. GraphQL type modifiers.

Selected valueSchema typeConsumer rule in this example
catalogItemNullable Product resultMissing key is malformed; explicit null is unavailable
idID!Keep the string and compare it with the requested ID
titleString!Require a string; an empty string passes this type check
statusProductStatus!Recognize the four current values; pause on an unknown value
updatedAtDateTime!Keep the original string; additionally require a parseable timezone-bearing value

See the selected Product fields. GraphQL serializes ID as a string, and Shopify's DateTime represents an ISO 8601 date-time string. The timestamp parser below is a consumer restriction, not a complete ISO 8601 conformance test. GraphQL scalar types, Shopify DateTime.

Do not use updatedAt alone to conclude that the title changed. Shopify documents that inventory adjustments caused by orders can also update this timestamp. Product.updatedAt.

Reject unusable envelopes before checking product fields

Shopify can return HTTP 200 with GraphQL errors. Execution errors may coexist with partial data; request errors before execution can leave data absent. Shopify response errors, GraphQL response semantics.

For this complete-snapshot consumer, any non-empty error list prevents promotion. A UI that intentionally displays partial data could use another policy, but would need to define which fields remain trustworthy.

SignalGate outcomeInvestigation
Non-200 response or invalid JSONStop before field validationTransport, service response, and request identifiers
Executed version differs from the requested versionHold the snapshotRequest URL and version support
Non-empty errors, with or without product dataRetain the previous valid snapshotError code, authorization, throttling, or service failure
catalogItem key is absentTreat the shape as incompatibleQuery alias, caching layer, and parser
catalogItem is explicitly nullMark unavailableResource ID, access, and current resource state

Null alone is insufficient evidence to delete a local product. Keep that decision separate from the response gate.

Capture the actual X-Shopify-API-Version response header, using case-insensitive header lookup in the calling layer. Shopify falls forward to the oldest supported version when a requested version is no longer supported. Therefore, record both the version in the request and the version actually used. Version support and response headers.

Run an offline gate with explicit outcomes

This Python 3.11+ function takes an already parsed response. The calling layer supplies the HTTP status, executed version, and JSON; JSON decoding failure stops before this function. Success returns only the four allowed fields. Failures return a state and None, so an extra field cannot silently become part of the downstream projection.

from datetime import datetime

VERSION = "2026-10"
STATUSES = {"ACTIVE", "ARCHIVED", "DRAFT", "UNLISTED"}
FIELDS = ("id", "title", "status", "updatedAt")


def accept_product(http_status, actual_version, payload, requested_id):
    if http_status != 200:
        return "HTTP_FAILURE", None
    if actual_version != VERSION:
        return "VERSION_MISMATCH", None
    if not isinstance(payload, dict):
        return "BAD_ENVELOPE", None
    if "errors" in payload:
        errors = payload["errors"]
        if not isinstance(errors, list):
            return "BAD_ENVELOPE", None
        if errors:
            return "GRAPHQL_ERRORS", None
    data = payload.get("data")
    if not isinstance(data, dict) or "catalogItem" not in data:
        return "MISSING_RESULT", None
    item = data["catalogItem"]
    if item is None:
        return "UNAVAILABLE", None
    if not isinstance(item, dict) or any(k not in item for k in FIELDS):
        return "BAD_FIELDS", None
    if any(not isinstance(item[k], str) for k in FIELDS):
        return "BAD_TYPES", None
    if item["id"] != requested_id:
        return "ID_MISMATCH", None
    if item["status"] not in STATUSES:
        return "UNKNOWN_STATUS", None
    value = item["updatedAt"]
    try:
        stamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
        if "T" not in value or stamp.tzinfo is None:
            return "BAD_TIMESTAMP", None
    except ValueError:
        return "BAD_TIMESTAMP", None
    return "ACCEPT", {k: item[k] for k in FIELDS}

A minimal synthetic check is:

fixture = {"data": {"catalogItem": {
    "id": "gid://shopify/Product/1001",
    "title": "Example shirt",
    "status": "UNLISTED",
    "updatedAt": "2026-10-04T00:00:00Z"
}}}
assert accept_product(200, "2026-10", fixture,
                      "gid://shopify/Product/1001")[0] == "ACCEPT"
fixture["data"]["catalogItem"]["status"] = "SYNTHETIC_FUTURE_VALUE"
assert accept_product(200, "2026-10", fixture,
                      "gid://shopify/Product/1001")[0] == "UNKNOWN_STATUS"

Offline tests exercised valid statuses, empty titles, extra fields, null and missing results, partial data with errors, malformed types, mismatched IDs, unknown statuses, invalid or timezone-free timestamps, and HTTP/version mismatches. This validates the gate's local behavior; it does not validate a store's grant or a live query.

Use a documented enum change as an upgrade test

ProductStatus in 2026-10 contains ACTIVE, ARCHIVED, DRAFT, and UNLISTED. The UNLISTED status is visible from API version 2025-10 onward; older versions translate it to ACTIVE. A consumer written for the earlier three-value set can therefore break after a version change even when the query still validates. Silently treating an unknown status as active would conceal a visibility distinction. ProductStatus definitions and version behavior.

Keep a fixture for each known status and an invented unknown value for the hold path. Also keep channel publishing separate: the official definition says active products are not automatically published to sales channels.

An upgrade review should leave a reproducible package: old and new query versions, selected-field type and enum differences, sanitized fixtures, gate and fixture revisions, an owner for each failure state, and a way to restore the previous consumer. Complete acceptance for an environment only after comparing the target schema, passing the offline cases, and passing an authorized development-store response through the same gate. Record an unexecuted store check as pending.

For multi-page workloads, continue with GraphQL performance and data-query acceptance. This response gate checks a single result; pagination and batch completion need their own evidence.

Browse the WESWOO project portfolio