Skip to main content

Overview

Metafields provide a flexible, type-safe system for adding custom structured data to Spree resources. Unlike metadata which is simple JSON storage, metafields are schema-defined with strong typing, validation, and visibility controls. Metafields enable you to extend resources with custom attributes without modifying the database schema, making them ideal for:
  • Product specifications (manufacturer, material, dimensions)
  • Custom business logic fields
  • Integration data from external systems
  • User-specific custom attributes
  • Order metadata with validation
Metafields are available from Spree 5.2 onwards. For earlier versions, use metadata instead.
Starting with Spree 5.2, Metafields are the recommended default for storing custom product attributes, replacing the legacy Product Properties system. For new projects, use Metafields instead of Properties.Product Properties are still supported for backward compatibility (until Spree 6.0) but require enabling via config.product_properties_enabled = true. See the upgrade guide for migration details.

Architecture

The metafields system consists of two main models:

MetafieldDefinition

Defines the schema and rules for a metafield type. Think of it as a blueprint that specifies:
  • What data type the field contains
  • Which resource it can be attached to
  • Where it should be visible (admin, storefront, or both)
  • How it should be organized (namespace)
# Example: Define a manufacturer field for products
definition = Spree::MetafieldDefinition.create!(
  namespace: 'properties',
  key: 'manufacturer',
  name: 'Manufacturer',
  metafield_type: 'Spree::Metafields::ShortText',
  resource_type: 'Spree::Product',
  display_on: 'both'
)

Metafield

Stores the actual data values according to a MetafieldDefinition:
# Create a metafield instance with actual data
product = Spree::Product.first
metafield = product.metafields.create!(
  metafield_definition: definition,
  type: 'Spree::Metafields::ShortText',
  value: 'Wilson'
)
Every model that includes the Spree::Metafields concern automatically gets metafields, public_metafields, and private_metafields associations.

Data Types

Spree supports six data types for metafields:
TypeClassUse CaseExample
Short TextSpree::Metafields::ShortTextBrief text fieldsSKU codes, brand names, tags
Long TextSpree::Metafields::LongTextLonger text contentCare instructions, notes
Rich TextSpree::Metafields::RichTextFormatted HTML contentProduct descriptions with formatting
NumberSpree::Metafields::NumberNumeric valuesWeight, quantity, ratings
BooleanSpree::Metafields::BooleanTrue/false flagsIs featured, requires signature
JSONSpree::Metafields::JsonStructured dataConfiguration, complex objects

Type-Specific Behavior

Short Text & Long Text

# Automatically strips whitespace
product.set_metafield('custom.sku', '  ABC-123  ')
product.get_metafield('custom.sku').value # => "ABC-123"

Rich Text

# Uses ActionText for rich HTML content
product.set_metafield('custom.description', '<h1>Hello</h1><p>World</p>')
metafield = product.get_metafield('custom.description')
metafield.value.to_s # => "<h1>Hello</h1><p>World</p>"

Number

# Returns BigDecimal for precision
product.set_metafield('measurements.weight', '15.75')
metafield = product.get_metafield('measurements.weight')
metafield.serialize_value # => BigDecimal("15.75")

Boolean

# Converts various inputs to boolean
product.set_metafield('flags.featured', 'true')
metafield = product.get_metafield('flags.featured')
metafield.serialize_value # => true
metafield.csv_value # => "Yes" (localized)

JSON

# Validates JSON structure
product.set_metafield('custom.config', '{"theme": "dark", "size": 12}')
metafield = product.get_metafield('custom.config')
metafield.serialize_value # => {"theme" => "dark", "size" => 12}

Visibility Control

Metafields support three visibility levels via the display_on attribute:

both

Visible in both admin panel and storefront. Use for public product specifications that need management.

front_end

Visible only in storefront and Storefront API. Use for customer-facing data that shouldn’t clutter admin.

back_end

Visible only in admin panel and Platform API. Use for internal notes, integration IDs, or sensitive data.
# Create a public metafield
Spree::MetafieldDefinition.create!(
  key: 'brand',
  namespace: 'product',
  resource_type: 'Spree::Product',
  metafield_type: 'Spree::Metafields::ShortText',
  display_on: 'both'
)

# Create an internal-only metafield
Spree::MetafieldDefinition.create!(
  key: 'supplier_id',
  namespace: 'integration',
  resource_type: 'Spree::Product',
  metafield_type: 'Spree::Metafields::ShortText',
  display_on: 'back_end'
)

Scopes for Filtering

# Get only public metafields
product.public_metafields

# Get only admin metafields
product.private_metafields

# Query definitions
Spree::MetafieldDefinition.available_on_front_end
Spree::MetafieldDefinition.available_on_back_end

Namespaces

Namespaces organize metafields into logical groups, preventing key conflicts and improving organization:
# Product properties
product.set_metafield('properties.material', '100% Cotton')
product.set_metafield('properties.fit', 'Regular')

# Integration data
product.set_metafield('shopify.product_id', '12345')
product.set_metafield('google.category_id', '5432')

# Custom business logic
product.set_metafield('custom.requires_approval', 'true')
Namespace and key are automatically normalized: 'My Custom Namespace' becomes 'my_custom_namespace'.

Supported Resources

The following resources support metafields by default:
  • Spree::Product
  • Spree::Variant
  • Spree::Order
  • Spree::LineItem
  • Spree::OptionType
  • Spree::OptionValue
  • Spree::Taxon
  • Spree::Taxonomy
  • Spree::Asset
  • Spree::Image
  • Spree::Payment
  • Spree::PaymentMethod
  • Spree::PaymentSource
  • Spree::CreditCard
  • Spree::Shipment
  • Spree::ShippingMethod
  • Spree::CustomerReturn
  • Spree::Refund
  • Spree::StockItem
  • Spree::StockTransfer
  • Spree::Promotion
  • Spree::GiftCard
  • Spree::StoreCredit
  • Spree::Post
  • Spree::PostCategory
  • Spree::Store
  • Spree::CustomDomain
  • Spree::Address
  • Spree::NewsletterSubscriber
  • Spree::TaxRate
  • Spree.user_class

CRUD Operations

Creating Metafields

The simplest way to create or update metafields:
product = Spree::Product.first

# Creates definition if it doesn't exist, then creates/updates metafield
product.set_metafield('properties.manufacturer', 'Wilson')
product.set_metafield('properties.material', '90% Cotton 10% Elastan')
product.set_metafield('properties.fit', 'Loose')

Method 2: Using Nested Attributes

Useful when working with forms:
product.update(
  metafields_attributes: [
    {
      metafield_definition_id: manufacturer_definition.id,
      type: 'Spree::Metafields::ShortText',
      value: 'Wilson'
    },
    {
      metafield_definition_id: material_definition.id,
      type: 'Spree::Metafields::ShortText',
      value: '100% Cotton'
    }
  ]
)

Method 3: Direct Creation

For full control:
# First, create or find the definition
definition = Spree::MetafieldDefinition.find_or_create_by!(
  namespace: 'custom',
  key: 'brand',
  resource_type: 'Spree::Product'
) do |d|
  d.name = 'Brand'
  d.metafield_type = 'Spree::Metafields::ShortText'
  d.display_on = 'both'
end

# Then create the metafield
product.metafields.create!(
  metafield_definition: definition,
  type: definition.metafield_type,
  value: 'Nike'
)

Reading Metafields

# Get a specific metafield
metafield = product.get_metafield('properties.manufacturer')
metafield.value # => "Wilson"
metafield.name  # => "Manufacturer"
metafield.key   # => "properties.manufacturer"

# Check if metafield exists
product.has_metafield?('properties.manufacturer') # => true
product.has_metafield?(definition)                # => true

# Get all metafields
product.metafields              # All metafields
product.public_metafields       # Front-end visible only
product.private_metafields      # Back-end only

# Access serialized value
metafield = product.get_metafield('properties.is_featured')
metafield.value          # => "true" (string)
metafield.serialize_value # => true (boolean)

Updating Metafields

# Method 1: Using set_metafield (creates or updates)
product.set_metafield('properties.manufacturer', 'New Brand')

# Method 2: Using nested attributes
product.update(
  metafields_attributes: [
    {
      id: existing_metafield.id,
      metafield_definition_id: definition.id,
      value: 'Updated Value'
    }
  ]
)

# Method 3: Direct update
metafield = product.get_metafield('properties.manufacturer')
metafield.update!(value: 'Updated Brand')

Deleting Metafields

# Method 1: Set value to blank (auto-destroys)
product.update(
  metafields_attributes: [
    {
      id: metafield.id,
      value: '' # Automatically marked for destruction
    }
  ]
)

# Method 2: Explicit destroy flag
product.update(
  metafields_attributes: [
    {
      id: metafield.id,
      _destroy: true
    }
  ]
)

# Method 3: Direct deletion
metafield = product.get_metafield('properties.manufacturer')
metafield.destroy
When a metafield value is set to blank via nested attributes, it’s automatically marked for destruction. This is intentional behavior to prevent empty metafields.

Querying by Metafields

Spree provides helpful scopes for querying resources by metafield values:
# Find products with specific metafield key
products = Spree::Product.with_metafield_key('properties.manufacturer')

# Find products with specific metafield key and value
products = Spree::Product.with_metafield_key_value('properties.fit', 'Loose')

# Find products with specific metafield (by definition)
products = Spree::Product.with_metafield(manufacturer_definition)

# Complex queries using joins
Spree::Product
  .joins(:metafields)
  .where(
    spree_metafields: {
      metafield_definition_id: definition.id
    }
  )
  .where("spree_metafields.value ILIKE ?", "%cotton%")

API Integration

Platform API

Metafields are automatically included in Platform API responses:
{
  "data": {
    "id": "1",
    "type": "product",
    "attributes": {
      "name": "Ruby on Rails T-Shirt",
      // ... other attributes
    },
    "relationships": {
      "metafields": {
        "data": [
          {
            "id": "1",
            "type": "metafield"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "1",
      "type": "metafield",
      "attributes": {
        "name": "Manufacturer",
        "key": "properties.manufacturer",
        "type": "Spree::Metafields::ShortText",
        "display_on": "both",
        "value": "Wilson"
      }
    }
  ]
}

Storefront API

Only public metafields (with display_on set to both or front_end) are included:
{
  "data": {
    "id": "1",
    "type": "product",
    "attributes": {
      "name": "Ruby on Rails T-Shirt"
    },
    "relationships": {
      "metafields": {
        "data": [
          {
            "id": "1",
            "type": "metafield"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "1",
      "type": "metafield",
      "attributes": {
        "name": "Manufacturer",
        "key": "properties.manufacturer",
        "type": "Spree::Metafields::ShortText",
        "value": "Wilson"
      }
    }
  ]
}
The display_on attribute is intentionally excluded from Storefront API responses for security reasons.

Admin Panel Management

Managing Definitions

Navigate to Settings → Metafield Definitions in the admin panel:
  1. Click “New Metafield Definition”
  2. Select the resource type (Product, Variant, Order, etc.)
  3. Enter namespace and key
  4. Choose the data type
  5. Set visibility (both, front_end, back_end)
  6. Save

Managing Values

When editing a resource (e.g., a product), metafields appear in a dedicated section:
  1. Edit any resource that has metafield definitions
  2. Scroll to the “Metafields” section
  3. Fill in values for each defined metafield
  4. Save
The admin panel automatically builds empty metafield forms for all definitions, making it easy to add values.

Validation

MetafieldDefinition Validations

validates :namespace, :key, :name, :resource_type, presence: true
validates :metafield_type, presence: true,
          inclusion: { in: available_types }
validates :resource_type,
          inclusion: { in: available_resources }
validates :key, uniqueness: {
  scope: [:resource_type, :namespace],
  message: 'must be unique per resource type and namespace'
}

Metafield Validations

validates :metafield_definition, :type, :resource, :value, presence: true
validates :metafield_definition_id, uniqueness: {
  scope: [:resource_type, :resource_id],
  message: 'already exists for this resource'
}
validate :type_must_match_metafield_definition
Type-specific validations are also applied:
# Number metafields
validates :value, numericality: true

# JSON metafields
validate :value_must_be_valid_json

# Boolean metafields
normalizes :value, with: ->(value) { value.to_b.to_s }

Configuration

Adding Custom Metafield Types

Create a custom metafield type:
# app/models/my_app/metafields/url.rb
module MyApp
  module Metafields
    class Url < Spree::Metafield
      validates :value, format: { with: URI::DEFAULT_PARSER.make_regexp }

      normalizes :value, with: ->(value) { value.to_s.strip }

      def serialize_value
        URI.parse(value)
      rescue URI::InvalidURIError
        value
      end
    end
  end
end
Register it in an initializer:
# config/initializers/spree.rb
Rails.application.config.spree.metafield_types << 'MyApp::Metafields::Url'

Enabling Metafields for Custom Resources

# config/initializers/spree.rb
Rails.application.config.spree.metafield_enabled_resources << 'MyApp::CustomResource'

# app/models/my_app/custom_resource.rb
module MyApp
  class CustomResource < ApplicationRecord
    include Spree::Metafields
  end
end

Metafields vs Metadata vs Product Properties

Understanding when to use each:
FeatureMetafieldsMetadataProduct Properties (Legacy)
Available SinceSpree 5.2+All versionsPre-5.2 (deprecated)
Recommended ForNew projectsSimple key-valueLegacy projects only
StructureStrongly typed, schema-definedSimple JSON key-valueProduct-specific properties
ValidationType-specific validationNoneName/value pairs
VisibilityConfigurable (front/back/both)Fixed (public/private)Always public
API AccessAuto-included in APIRequires manual serializationAuto-included in API
Admin UIDedicated management UIDirect JSON editingDedicated UI
OrganizationNamespaced (namespace.key)Flat structureProperty-based
Data Types6 specific types with serializationAny JSON-serializable dataText only
QueryingBuilt-in query scopesManual JSON queriesBuilt-in queries
ResourcesAny enabled resourceAny resourceProducts only

When to Use Metafields

  • New projects starting with Spree 5.2+
  • You need type validation (numbers, booleans, URLs)
  • Different visibility for different audiences
  • Data appears in admin UI forms
  • You want query-optimized custom fields
  • You need organized namespacing
  • You want to extend multiple resource types

When to Use Metadata

  • Simple key-value storage without validation
  • Truly dynamic data with unknown structure
  • Integration payloads that change frequently
  • Quick prototyping without schema definition

When to Use Product Properties (Legacy)

  • Existing projects upgrading from pre-5.2 that aren’t ready to migrate
  • You need backward compatibility until Spree 6.0
  • You must enable via config.product_properties_enabled = true
Product Properties are deprecated and will be removed in Spree 6.0. For new projects, always use Metafields. For existing projects, plan to migrate using the migration rake task.

Common Use Cases

Product Specifications

# Define specifications namespace
manufacturer_def = Spree::MetafieldDefinition.create!(
  namespace: 'specifications',
  key: 'manufacturer',
  name: 'Manufacturer',
  resource_type: 'Spree::Product',
  metafield_type: 'Spree::Metafields::ShortText',
  display_on: 'both'
)

warranty_def = Spree::MetafieldDefinition.create!(
  namespace: 'specifications',
  key: 'warranty_years',
  name: 'Warranty (Years)',
  resource_type: 'Spree::Product',
  metafield_type: 'Spree::Metafields::Number',
  display_on: 'both'
)

# Set values
product.set_metafield('specifications.manufacturer', 'Sony')
product.set_metafield('specifications.warranty_years', '2')

Integration with External Systems

# Store Shopify IDs (admin only)
Spree::MetafieldDefinition.create!(
  namespace: 'shopify',
  key: 'product_id',
  name: 'Shopify Product ID',
  resource_type: 'Spree::Product',
  metafield_type: 'Spree::Metafields::ShortText',
  display_on: 'back_end'
)

product.set_metafield('shopify.product_id', '12345678')

# Query products by Shopify ID
Spree::Product.with_metafield_key_value('shopify.product_id', '12345678')

Feature Flags

# Create feature flag definitions
Spree::MetafieldDefinition.create!(
  namespace: 'flags',
  key: 'featured',
  name: 'Featured Product',
  resource_type: 'Spree::Product',
  metafield_type: 'Spree::Metafields::Boolean',
  display_on: 'back_end'
)

# Set flags
product.set_metafield('flags.featured', 'true')

# Query featured products
featured_products = Spree::Product
  .with_metafield_key('flags.featured')
  .joins(:metafields)
  .where(spree_metafields: { value: 'true' })

Custom Order Data

# Store gift message (visible to admins)
Spree::MetafieldDefinition.create!(
  namespace: 'custom',
  key: 'gift_message',
  name: 'Gift Message',
  resource_type: 'Spree::Order',
  metafield_type: 'Spree::Metafields::LongText',
  display_on: 'back_end'
)

order.set_metafield('custom.gift_message', 'Happy Birthday!')

# Store delivery instructions (visible to customers)
Spree::MetafieldDefinition.create!(
  namespace: 'delivery',
  key: 'instructions',
  name: 'Delivery Instructions',
  resource_type: 'Spree::Order',
  metafield_type: 'Spree::Metafields::LongText',
  display_on: 'both'
)

order.set_metafield('delivery.instructions', 'Leave at front door')

Best Practices

  1. Use Descriptive Namespaces: Group related metafields (e.g., specifications.*, integration.*, custom.*)
  2. Choose Appropriate Visibility: Don’t expose internal data to the storefront
  3. Validate Data Types: Use the correct metafield type for your data
  4. Keep Keys Consistent: Use snake_case for keys, they’re automatically normalized
  5. Document Custom Types: If creating custom metafield types, document their behavior
  6. Query Efficiently: Use the provided scopes instead of custom joins when possible
  7. Handle Missing Values: Always check if get_metafield returns nil
  8. Use Set Metafield for Simplicity: Prefer set_metafield over manual creation
  9. Consider Performance: Eager load metafields when displaying many resources:
# ✅ Good - eager loads metafields
products = Spree::Product.includes(:metafields).limit(20)

# ❌ Bad - N+1 queries
products = Spree::Product.limit(20)
products.each { |p| p.metafields.to_a }
  1. Migrate from Properties: If upgrading from pre-5.2, use the migration rake task