Shopify Metaobjects vs Metafields: Which to Use

Use a metafield when you need one extra value on something Shopify already models: a product, a customer, an order. Use a metaobject when the thing you are describing has several fields of its own and gets reused across many products. Metafields extend records. Metaobjects are records.

The Shopify metaobjects vs metafields decision usually goes wrong in one direction. A store ends up with size_guide_title, size_guide_body and size_guide_image copied onto forty products, when what it wanted was one size guide entry that forty products point at.

Everything below was written against Admin GraphQL and Storefront API version 2026-07, current as of September 2026.

Shopify metaobjects vs metafields: what actually differs

A metafield is a typed value hung off an owner resource. Namespace, key, type, value. It exists only because its owner exists, so deleting the product takes the metafield with it.

A metaobject has no owner. It has a type (size_guide), a handle (womens-outerwear) and up to 40 fields of its own. It can be drafted or published, translated, given a URL, and listed in the sitemap. Nothing needs to point at it for it to exist.

The two meet through a reference type. A metaobject_reference metafield on the product stores a pointer to one entry, and list.metaobject_reference stores up to 1,024 of them. That reference is the join, and it is the piece people miss when they try to model a whole content structure out of flat metafields.

A rough test that holds up in practice: if you would give it its own database table, it is a metaobject. If it would be a column on a table you already have, it is a metafield.

Definitions come first. Values written against a definition that does not exist are rejected.

mutation CreateSizeGuide($definition: MetaobjectDefinitionCreateInput!) {
  metaobjectDefinitionCreate(definition: $definition) {
    metaobjectDefinition { id type }
    userErrors { field message code }
  }
}
{
  "definition": {
    "type": "size_guide",
    "name": "Size guide",
    "displayNameKey": "title",
    "access": { "storefront": "PUBLIC_READ" },
    "capabilities": { "publishable": { "enabled": true } },
    "fieldDefinitions": [
      { "key": "title", "name": "Title", "type": "single_line_text_field", "required": true },
      { "key": "body", "name": "Body", "type": "rich_text_field" },
      { "key": "chart", "name": "Chart image", "type": "file_reference" }
    ]
  }
}

Storefront access defaults to none, per the metaobject definition docs. Leave it out and the definition behaves perfectly in the admin while returning nothing whatsoever to your theme or your Hydrogen loader. That is a confusing hour to lose.

Then the metafield that points at it, through metafieldDefinitionCreate:

{
  "definition": {
    "name": "Size guide",
    "namespace": "custom",
    "key": "size_guide",
    "type": "metaobject_reference",
    "ownerType": "PRODUCT",
    "access": { "storefront": "PUBLIC_READ" },
    "validations": [
      {
        "name": "metaobject_definition_id",
        "value": "gid://shopify/MetaobjectDefinition/1234567890"
      }
    ]
  }
}

The metaobject_definition_id validation is what stops a merchant attaching a care instruction entry to a size guide field. Skip it and the field accepts any metaobject at all.

Building an embedded app rather than configuring one store? Declare the same shapes in shopify.app.toml and let them install with the app:

[metaobjects.app.size_guide]
name = "Size guide"
display_name_field = "title"
access.storefront = "public_read"

[metaobjects.app.size_guide.fields.title]
name = "Title"
type = "single_line_text_field"
required = true

[product.metafields.app.size_guide]
name = "Size guide"
type = "metaobject_reference<$app:size_guide>"

App-owned definitions sit in the reserved $app namespace, which means they are version controlled and you never hand-roll a migration script. Query them with namespace: "$app". Writing "app" is a common and silent mistake.

How do you write and read entries?

Writing is one mutation, keyed on the handle, so re-running it updates rather than duplicates:

mutation UpsertSizeGuide(
  $handle: MetaobjectHandleInput!
  $metaobject: MetaobjectUpsertInput!
) {
  metaobjectUpsert(handle: $handle, metaobject: $metaobject) {
    metaobject { id handle }
    userErrors { field message code }
  }
}

Variables: {"handle": {"type": "size_guide", "handle": "womens-outerwear"}, "metaobject": {"fields": [{"key": "title", "value": "Womens outerwear"}], "capabilities": {"publishable": {"status": "ACTIVE"}}}}.

Reading it on the storefront goes through the reference rather than a second round trip:

query ProductSizeGuide($handle: String!) @inContext(country: GB, language: EN) {
  product(handle: $handle) {
    sizeGuide: metafield(namespace: "custom", key: "size_guide") {
      reference {
        ... on Metaobject {
          handle
          title: field(key: "title") { value }
          body: field(key: "body") { value }
        }
      }
    }
  }
}

Use reference for the single type and references(first: 10) for the list type. Fetching entries as part of the product query keeps you inside one request against the same cost-based throttle that governs cart create and update mutations. Querying entries directly through the top-level metaobjects(type:) field needs the unauthenticated_read_metaobjects access scope.

How do you render one in a theme?

Liquid resolves the reference for you when you take .value:

{% assign guide = product.metafields.custom.size_guide.value %}
{% if guide %}
  <h2>{{ guide.title }}</h2>
  {{ guide.body }}
{% endif %}

Two things bite here. shop.metaobjects is deprecated and has been replaced by the top-level metaobjects object, so a global lookup is now {{ metaobjects.size_guide['womens-outerwear'].title }}. And if you enabled the publishable capability, an entry with DRAFT status returns nil in Liquid, which looks exactly like a broken reference until you check the status in the admin.

Two further capabilities are worth knowing about. renderable exposes SEO fields to Liquid and the Storefront API and puts entries in the store sitemap. onlineStore works with it to give entries a theme template and a real URL, in the form /pages/{urlHandle}/{entry-handle}. Between them, metaobjects stop being product decoration and start being pages.

What limits will you hit?

The ceilings are high enough that most builds never touch them, but the shapes matter. A metaobject definition holds up to 40 fields and up to 1,000,000 entries. An app can create 128 metaobject definitions per shop; merchants get 128 on Basic, Shopify and Advanced, and 256 on Plus and Enterprise.

Metafields are more generous on definitions and tighter on payloads. Both apps and merchants get 256 definitions per resource type. Most metafield types cap the value at 64KB, with json at 128KB and id and url at 2KB. List types hold 128 items, apart from metaobject references at 1,024.

The 40-field ceiling is the one that shapes designs. Reach for a second definition and a reference between them long before you get near it.

When is a metafield still the right answer?

When the value is genuinely one value and genuinely belongs to that record. A launch date, an internal SKU, a care symbol code, a boolean that switches a badge on. Single scalars that differ per product should stay metafields, because a metaobject entry per product gives you the duplication you were trying to avoid, plus a second hop on every query.

Metafields also win where the platform reads them for you. Shopify Functions take metafields through the input query, so discount and delivery logic that keys off a variant attribute is simpler as a metafield on the variant.

Model the reusable content as metaobjects, keep the per-record scalars as metafields, and define both before you write a single value. If you are already running a theme with a handful of custom fields and no repetition anywhere, adding a metaobject layer buys you nothing. The moment the same block of copy appears on a second product, move it.

Whoooop builds Shopify storefronts and the data models underneath them, whether that is an Online Store 2.0 theme reading metaobjects through Liquid or a headless build weighing Hydrogen against Next.js. If you need custom data modelled once and modelled properly, our Shopify development work is the place to start.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch