The problem they solve
Every Shopify product has a title, description, price, images, and variants. That's often not enough. A furniture store needs materials, dimensions, and care instructions. A skincare brand needs ingredient lists, certifications, and skin-type recommendations. A B2B store needs per-customer pricing tiers and lead times.
Before Metafields were first-class citizens (properly exposed in the admin and Liquid), the workaround was stuffing this data into product descriptions with custom formatting, or building private app integrations just to hold extra fields. Both are terrible.
Metafields are Shopify's built-in solution: arbitrary key/value data attached to any resource (products, variants, collections, pages, orders, customers, the shop itself). Metaobjects take it further — they're standalone custom content types, like a headless CMS built into Shopify.
Metafields vs Metaobjects — when to use which
| Metafields | Metaobjects | |
|---|---|---|
| Attached to | An existing resource (product, variant, collection, page…) | Standalone — not tied to a specific product or page |
| Best for | Extra attributes on existing content: ingredients, dimensions, specs | Independent content types: team members, testimonials, FAQs, size guides |
| Admin UI | Appears in the product/page/collection edit screen | Has its own section in Content → Metaobjects |
| References | Can reference a Metaobject or another resource | Can be referenced by metafields or other metaobjects |
| Liquid access | product.metafields.namespace.key |
metaobject.field_key.value after fetching via reference or all_products |
In practice: use Metafields to enrich existing resources, use Metaobjects when you need content that lives on its own — a library of entries a merchant manages centrally and references from multiple places.
Setting up Metafield definitions
You can create metafields ad-hoc (just set a value via API and it exists), but that's messy. The right approach is to define them first — this registers the field with a type, validation rules, and makes it appear as a proper editable field in the Shopify admin.
Go to Settings → Custom data → Products (or whichever resource) → Add definition.
What you configure per definition:
- Namespace — a grouping prefix. Use something consistent like
customfor merchant-managed fields or your app/theme name for programmatic ones. - Key — the identifier used in Liquid:
product.metafields.custom.care_instructions - Type — this matters a lot. Shopify has rich types:
single_line_text_field,multi_line_text_field,rich_text_field,number_decimal,boolean,date,color,url,file_reference,product_reference,metaobject_reference, and list variants of most. - Storefront access — must be explicitly enabled per definition if you want to read it via Storefront API or in Liquid on the frontend.
Type choice has downstream consequences. A rich_text_field returns a JSON object in Liquid, not a plain string — you need the metafield_tag filter or manual parsing to render it. A file_reference returns a file object. Choose the right type from the start; changing it later is destructive.
Reading Metafields in Liquid
The basic pattern is simple:
{%- comment -%} Access a plain text metafield {%- endcomment -%} {{ product.metafields.custom.care_instructions }} {%- comment -%} With a fallback {%- endcomment -%} {{ product.metafields.custom.care_instructions | default: 'See packaging for care details' }} {%- comment -%} Conditional rendering — only output if value exists {%- endcomment -%} {%- if product.metafields.custom.care_instructions != blank -%} <div class="care-instructions"> {{ product.metafields.custom.care_instructions }} </div> {%- endif -%}
Rich text metafields
Rich text is stored as a Shopify-specific JSON structure. To render it as HTML, use the metafield_tag filter:
{{ product.metafields.custom.extended_description | metafield_tag }}
This outputs clean HTML — headings, paragraphs, lists, bold/italic — that the merchant wrote in the rich text editor. It's the closest thing to a built-in WYSIWYG for product content.
File reference metafields
A file_reference metafield returns a file object. For images:
{%- assign badge = product.metafields.custom.trust_badge.value -%} {%- if badge != blank -%} {{ badge | image_url: width: 120 | image_tag: alt: badge.alt, loading: 'lazy' }} {%- endif -%}
List metafields
List types return arrays, so you iterate them:
{%- assign features = product.metafields.custom.key_features.value -%} {%- if features != blank -%} <ul class="features-list"> {%- for feature in features -%} <li>{{ feature }}</li> {%- endfor -%} </ul> {%- endif -%}
Metaobjects — building content types
A Metaobject definition is a schema: you define a set of fields (each with a type), and merchants create as many entries as they need. Think of it like a custom post type in WordPress, but native to Shopify.
Example use case: a Testimonial metaobject type with fields: author_name (single_line_text), quote (multi_line_text), rating (number_integer), product_reference (product_reference).
Create the definition at Settings → Custom data → Metaobjects → Add definition. Merchants then add testimonial entries from Content → Metaobjects.
Referencing Metaobjects from products
The most common pattern: a product has a metafield of type metaobject_reference (or a list of them) pointing to metaobject entries. This links, say, a product to its size guide, or a collection to a set of featured testimonials.
{%- comment -%} Product has a 'size_guide' metafield of type metaobject_reference → SizeGuide type {%- endcomment -%} {%- assign guide = product.metafields.custom.size_guide.value -%} {%- if guide != blank -%} <div class="size-guide"> <h3>{{ guide.fields.title.value }}</h3> {{ guide.fields.content.value | metafield_tag }} </div> {%- endif -%}
Rendering a list of Metaobject entries in a section
For a testimonials section that the merchant controls entirely through the Metaobjects admin:
{%- comment -%} In your section schema, expose a metaobject_reference list setting. Or fetch all entries of a type and iterate. Here we use a list metafield on the shop object. {%- endcomment -%} {%- assign testimonials = shop.metafields.custom.featured_testimonials.value -%} {%- if testimonials != blank -%} <ul class="testimonials"> {%- for item in testimonials -%} <li class="testimonial-card"> <blockquote>{{ item.fields.quote.value }}</blockquote> <cite>{{ item.fields.author_name.value }}</cite> {%- if item.fields.rating.value != blank -%} <div class="rating" aria-label="{{ item.fields.rating.value }} out of 5"> {{ item.fields.rating.value }}/5 </div> {%- endif -%} </li> {%- endfor -%} </ul> {%- endif -%}
Exposing Metaobjects as section settings
You can let merchants pick a specific metaobject entry directly in the theme editor, without writing any Liquid logic for fetching. In your section schema:
{
"type": "metaobject",
"id": "promo_banner",
"label": "Promo banner content",
"metaobject_type": "promo_banner"
}
Then in your section Liquid:
{%- assign banner = section.settings.promo_banner -%} {%- if banner != blank -%} <div class="promo-banner" style="background-color: {{ banner.fields.bg_color.value }}"> <p>{{ banner.fields.message.value }}</p> {%- if banner.fields.cta_url.value != blank -%} <a href="{{ banner.fields.cta_url.value }}"> {{ banner.fields.cta_label.value | default: 'Shop now' }} </a> {%- endif -%} </div> {%- endif -%}
This is the pattern I reach for most. The merchant manages the content in the Content admin. The developer controls the schema and the rendering. Neither has to touch the other's domain.
Storefront API access
If you're building headless or using JavaScript to fetch product data (for example, via the Ajax API for a quick-add drawer), metafields don't come back in the standard product JSON by default. You need to use the Storefront API with a GraphQL query, or use the fields parameter on the REST endpoints if you're accessing them server-side.
For client-side JavaScript access, the Storefront API is the right tool:
query ProductMetafields($handle: String!) { productByHandle(handle: $handle) { title metafield(namespace: "custom", key: "care_instructions") { value type } # For a list metafield: keyFeatures: metafield(namespace: "custom", key: "key_features") { value # returns JSON array string type } } }
Storefront access must be enabled. Each metafield definition has a "Storefront access" toggle in the admin. If it's off, the Storefront API returns null for that field — not an error, just null. This is the most common gotcha when metafields work in Liquid but not in API responses.
Common patterns and practical tips
Namespace conventions
Use consistent namespaces across a project. My convention:
custom— merchant-managed content fields (care instructions, specs, badges)theme— fields the theme reads but that aren't meant for general CMS useapp.[appname]— fields owned by a specific app integration
Always check for blank before rendering
Metafields return nil if undefined or empty. Always guard with != blank before outputting to avoid rendering empty tags or broken layout sections.
Use .value for complex types
For reference types (file, product, metaobject) and list types, you need .value to get the actual usable object. Without it, you're looking at the raw metafield object, not its content. Plain text and number fields don't need .value — they output directly.
{%- comment -%} Plain text — no .value needed {%- endcomment -%} {{ product.metafields.custom.subtitle }} {%- comment -%} File reference — .value gives the file object {%- endcomment -%} {%- assign icon = product.metafields.custom.category_icon.value -%} {{ icon | image_url: width: 48 | image_tag: loading: 'lazy' }}
Metaobjects as a page builder replacement
One of the most powerful uses: create a landing_section metaobject type with fields for heading, body copy, image, CTA label, and CTA URL. Merchants build "pages" by creating an ordered list of these entries and attaching it to a page metafield. The theme iterates and renders each section. This gives merchants a lightweight page builder without a third-party app.
Metafields + Metaobjects replace a large class of custom app requirements. Before reaching for a private app or a third-party CMS integration, check whether the data model fits natively. In most content-driven use cases, it does.