DEV Community

FlareCanary
FlareCanary

Posted on

shopify.metafields returns undefined in my checkout extension after 2026-04 — here's why

If you just bumped your checkout UI extension to Shopify API version 2026-04 and your shopify.metafields reads started returning undefined, you've hit the biggest breaking change in this release.

Checkout metafields in the metafields API are gone. They've been replaced by order metafields in the appMetafields API. The rename isn't cosmetic — it's a different data source, a different access pattern, and a different set of timing semantics. The old code doesn't error; it just silently returns nothing.

What the failure looks like

The code that worked on 2026-01:

import { extension } from '@shopify/ui-extensions/checkout';

export default extension('purchase.checkout.block.render', (root, api) => {
  const { metafields } = api;
  const myField = metafields.current.find(
    m => m.namespace === 'custom' && m.key === 'delivery_window'
  );
  // myField.value used to be "2026-05-01T09:00:00"
  root.append(root.createText(myField?.value ?? 'unknown'));
});
Enter fullscreen mode Exit fullscreen mode

After the 2026-04 upgrade, myField is undefined. No deprecation warning. No network error. The extension renders 'unknown' in production and the fallback path becomes the default behavior. If the ternary hid a real problem — a missing delivery window, a missing subscription flag, a missing audit tag — your orders flow through without it.

The symptom in Shopify's developer console, if you're lucky enough to be looking there at the right moment, is either a missing metafield in the api.metafields.current array or the array itself being empty. Neither is an error condition, just a change in what's served.

Why it breaks silently

Shopify's UI extensions surface APIs as plain JS properties. When the platform removes a property at a given API version, the consuming code reads undefined — the same as if the field weren't set for this buyer. There's no distinction between "metafield doesn't exist on this order" and "this API doesn't expose metafields anymore," because both are the same runtime shape.

The 2026-04 release moves this data to a different surface: shopify.appMetafields. The old call site keeps compiling because metafields is still a property on api — it's just the thing it references has shrunk.

The exact migration

The replacement property is appMetafields, read from the global shopify object rather than a callback parameter:

import '@shopify/ui-extensions/preact';
import { render } from 'preact';

export default async () => {
  render(<Extension />, document.body);
};

function Extension() {
  const entries = shopify.appMetafields.value;
  const myField = entries.find(
    e =>
      e.target.type === 'order' &&
      e.metafield.namespace === 'custom' &&
      e.metafield.key === 'delivery_window'
  );
  return <s-text>{myField?.metafield.value ?? 'unknown'}</s-text>;
}
Enter fullscreen mode Exit fullscreen mode

Three differences worth internalizing:

  1. appMetafields is an app-scoped surface. Your extension only sees metafields written under your app's reserved namespace. Anything your app wrote to a custom.* namespace before is visible on the compatibility shim today, but new writes should use the app-reserved namespace going forward.
  2. Entries are wrapped, not bare. Each entry has { target, metafield }. The target is the resource the metafield is attached to (order, customer, etc.). The old flat-array shape is gone — filter on target.type before filtering on namespace/key.
  3. It's a reactive value, not a snapshot. shopify.appMetafields is a Preact-signals-style reactive value. Reading .value gives the current array. Inside a component it re-renders when the underlying entries change, which matches the new 2026-01 UI model (Preact web components replacing React).

Where the old pattern is hiding in your code

This migration hits more than the obvious metafield calls. Places to grep:

git grep -n "api\.metafields"
git grep -n "useMetafields"
git grep -n "shopify\.metafields"
git grep -n "ExtensionPoint.*Checkout"
Enter fullscreen mode Exit fullscreen mode

The useMetafields React hook is an older API that also routed through the deprecated surface. Anywhere you called it, the replacement is the shopify.appMetafields.value read above.

Server-side writers don't need to change — the Admin GraphQL API's metafieldsSet mutation still works. It's only the read path from checkout extensions that moves.

The cart-metafields path, if you're writing fresh code

Separately from the appMetafields migration, Shopify is steering new code toward cart metafields as the canonical place for per-buyer data that needs to survive checkout. As of 2026-04, cart metafields automatically copy to the resulting order at checkout completion, which closes a long-standing gap where checkout-time state quietly didn't make it to fulfillment.

If you're building something new today, the decision tree is:

  • Per-buyer data that needs to outlive checkout → cart metafields. They'll auto-copy to the order.
  • Per-app configuration that needs to be read at checkout → app-reserved namespace metafields, read via shopify.appMetafields.
  • Legacy custom.*-namespace metafields your app has historically written → still readable on the compatibility shim, but plan a migration to your app-reserved namespace.

Detecting the break before your QA does

The obvious version check:

grep -r "api_version" extensions/ | grep -v node_modules
Enter fullscreen mode Exit fullscreen mode

If anything returns 2026-04 or later and the extension still reads api.metafields, that extension is broken.

The less-obvious version: extensions can be bumped independently, and an app with multiple extensions can have one on 2026-04 and another on 2026-01. The one on 2026-04 silently fails; the one on 2026-01 works. You get half your data on the order and half missing — exactly the confusing support ticket that's hard to reproduce.

A small synthetic order run in your staging shop is the cheapest catch. Place an order that exercises each extension, pull the resulting order via the Admin API, and diff the metafields array against what you expect. Any missing key is a lead.

The broader pattern

What's happening here is a specific case of a general problem: the SDK you depend on changes the shape of the data it returns, not the shape of its function signatures. Types still line up. The code still compiles. The values have just moved, and the old read path becomes undefined.

The ways this usually bites:

  • TypeScript doesn't catch it. The types got updated on the SDK, your code got updated with @shopify/ui-extensions 2026-04, and api.metafields is still typed — it's just a narrower universe of things now.
  • Unit tests don't catch it. Mocked responses in a test fixture don't know the real platform shape changed.
  • CI doesn't catch it. Nothing in CI is hitting Shopify's edge. The break only shows up in a real checkout.

The way to catch this kind of drift before buyers do is a scheduled synthetic check against the real vendor surface — place an order in a staging shop, read it back, diff the metafields array against a known baseline. Same pattern as monitoring any other third-party API contract, applied to the specific surface each platform exposes.

Minimum-viable fix for today

  1. git grep -E "api_version.*2026-04" across every extension and confirm which ones bumped
  2. git grep -n "api\.metafields\|useMetafields" in every bumped extension — each hit is a broken read
  3. Migrate those call sites to shopify.appMetafields.value with .target.type === 'order' filtering
  4. Move any app-owned metafield writes to your app-reserved namespace
  5. Place a synthetic test order in a staging shop and verify the expected metafield keys land on the resulting order
  6. If you have multiple extensions on mixed API versions, align them or at minimum document which are on which version

If the migration reveals that checkout metafields were load-bearing for fulfillment, consider whether cart metafields — now auto-copied to the order at checkout completion — are the better home going forward.


FlareCanary monitors REST and GraphQL APIs — including Shopify Admin and Storefront endpoints — for schema drift. Free tier covers 5 endpoints with daily checks.

Top comments (0)