obsidian/raw/_processed/plugins__import-export.md
2026-05-15 16:18:46 +01:00

39 KiB

title label order desc keywords source
Import Export Plugin Import Export 40 Add Import and export functionality to create CSV and JSON data exports plugins, plugin, import, export, csv, JSON, data, ETL, download https://payloadcms.com/docs/plugins/import-export

!410ba741e3f5fcfea1aa0e579172b5b1_MD5.svg

This plugin adds features that give admin users the ability to download or create export data as an upload collection and import it back into a project.

Core Features

  • Export data as CSV or JSON format via the admin UI
  • Download the export directly through the browser
  • Create a file upload of the export data
  • Use the jobs queue for large exports
  • Import collection data
  • Preview data before exporting or importing

Installation

Install the plugin using any JavaScript package manager like pnpm, npm, or Yarn:

pnpm add @payloadcms/plugin-import-export

Basic Usage

In the plugins array of your Payload Config, call the plugin with options:

import { buildConfig } from 'payload'
import { importExportPlugin } from '@payloadcms/plugin-import-export'

const config = buildConfig({
  collections: [Users, Pages],
  plugins: [
    importExportPlugin({
      collections: [{ slug: 'users' }, { slug: 'pages' }],
      // see below for a list of available options
    }),
  ],
})

export default config

Jobs Queue Requirements

By default, the plugin uses Payload's Jobs Queue for import and export operations. Queued jobs require a runner to be configured, otherwise imports and exports will stay in "pending" status and never complete.

Configure jobs.autoRun in your Payload config so that queued jobs are processed:

import { buildConfig } from 'payload'
import { importExportPlugin } from '@payloadcms/plugin-import-export'

const config = buildConfig({
  collections: [Users, Pages],
  jobs: {
    autoRun: [
      {
        cron: '*/5 * * * *', // Check every 5 minutes
        queue: 'default',
      },
    ],
  },
  plugins: [
    importExportPlugin({
      collections: [{ slug: 'users' }, { slug: 'pages' }],
    }),
  ],
})

export default config

Alternatively, use allQueues: true to process jobs from all queues:

jobs: {
  autoRun: [
    {
      allQueues: true,
      cron: '*/5 * * * *',
    },
  ],
},

For smaller datasets or testing, you can run imports and exports synchronously by setting disableJobsQueue: true in the per-collection ExportConfig or ImportConfig. This avoids the need for a jobs runner but blocks the request until the operation completes.

Options

Property Type Description
collections array Collections to include Import/Export controls in. Array of collection configs with per-collection options. Defaults to all.
debug boolean If true, enables debug logging.
exportLimit number | function Global maximum documents for export operations. Set to 0 for unlimited (default). Per-collection limits take precedence.
importLimit number | function Global maximum documents for import operations. Set to 0 for unlimited (default). Per-collection limits take precedence.
overrideExportCollection function Function to override the default export collection. Receives { collection } and returns modified collection config.
overrideImportCollection function Function to override the default import collection. Receives { collection } and returns modified collection config.

Per-Collection Configuration

Each item in the collections array can have the following properties:

Property Type Description
slug string The collection slug to configure.
export boolean | ExportConfig Set to false to disable export, or provide export-specific options.
import boolean | ImportConfig Set to false to disable import, or provide import-specific options.

ExportConfig Options

Property Type Description
batchSize number Documents per batch during export. Default: 100.
disableDownload boolean Disable download button for this collection.
disableJobsQueue boolean Run exports synchronously for this collection.
disableSave boolean Disable save button for this collection.
format string Force format (csv or json) for this collection.
hooks object Lifecycle hooks — before and after. See Hooks.
limit number | function Maximum documents to export. Set to 0 for unlimited (default). Overrides global exportLimit.
overrideCollection function Override the export collection config for this specific target.

ImportConfig Options

Property Type Description
batchSize number Documents per batch during import. Default: 100.
defaultVersionStatus string Default status for imported docs (draft or published).
disableJobsQueue boolean Run imports synchronously for this collection.
hooks object Lifecycle hooks — before and after. See Hooks.
limit number | function Maximum documents to import. Set to 0 for unlimited (default). Overrides global importLimit.
overrideCollection function Override the import collection config for this specific target.

Example Configuration

import { importExportPlugin } from '@payloadcms/plugin-import-export'

export default buildConfig({
  plugins: [
    importExportPlugin({
      debug: true,

      // Global limits (0 = unlimited, which is the default)
      exportLimit: 10000,
      importLimit: 5000,

      // Override default export collection (e.g., add access control)
      // This will be used by all collections unless they further override the config
      overrideExportCollection: ({ collection }) => {
        collection.access = {
          ...collection.access,
          read: ({ req }) => req.user?.role === 'admin',
        }
        return collection
      },

      // Per-collection settings
      collections: [
        {
          slug: 'pages',
          export: {
            format: 'csv',
            disableDownload: true,
            limit: 1000, // Override global exportLimit for this collection
          },
          import: {
            defaultVersionStatus: 'draft',
            limit: 500, // Override global importLimit for this collection
          },
        },
        {
          slug: 'posts',
          export: false, // Disable export for posts
        },
      ],
    }),
  ],
})

Collection Visibility

The exports and imports collections are hidden from the admin navigation by default (admin.group: false). Their routes remain accessible (for example /admin/collections/exports and /admin/collections/imports), so you can open them directly or link to them from your app. To list them in the sidebar, use overrideExportCollection and overrideImportCollection to set a group:

import { importExportPlugin } from '@payloadcms/plugin-import-export'

importExportPlugin({
  overrideExportCollection: ({ collection }) => ({
    ...collection,
    admin: {
      ...collection.admin,
      group: 'Data Management',
    },
  }),
  overrideImportCollection: ({ collection }) => ({
    ...collection,
    admin: {
      ...collection.admin,
      group: 'Data Management',
    },
  }),
  collections: [{ slug: 'pages' }],
})

With a group set, the Exports and Imports collections appear in the admin navigation under "Data Management", and you can open saved exports or import documents from there (including downloading saved export files).

Limiting Import and Export Size

You can limit the number of documents that can be imported or exported in a single operation. This helps prevent DDOS-style abuse and protects server resources during large data operations.

Limits can be set globally via exportLimit and importLimit, or per-collection using the limit option in export/import config. Per-collection limits take precedence over global limits. Set to 0 for unlimited (the default).

Dynamic Limits

Limits can be a function that receives the request context, allowing dynamic limits based on user roles or other factors:

importExportPlugin({
  // Dynamic global limit based on user role
  exportLimit: ({ req }) => {
    return req.user?.role === 'admin' ? 50000 : 1000
  },

  collections: [
    {
      slug: 'sensitive-data',
      import: {
        // Per-collection dynamic limit
        limit: ({ req }) => {
          if (req.user?.subscription === 'enterprise') return 100000
          if (req.user?.subscription === 'pro') return 10000
          return 1000
        },
      },
    },
  ],
})

Collection-Specific Import and Export targets

By default, the plugin creates a single exports collection and a single imports collection that handle all import/export operations across your enabled collections. However, you can create separate import and export targets for specific collections by overriding the collection slug.

When you change the slug using the overrideCollection function at the per-collection level, this creates an entirely separate uploads collection for that specific source collection. This is useful when you need:

  • Different access control rules for different data types
  • Separate storage locations for exports
  • Isolated import queues for specific workflows
  • Different admin UI organization

Example: Separate Export Targets

import { importExportPlugin } from '@payloadcms/plugin-import-export'

export default buildConfig({
  plugins: [
    importExportPlugin({
      collections: [
        {
          slug: 'users',
          export: {
            // Create a separate 'user-exports' collection for user data
            overrideCollection: ({ collection }) => {
              return {
                ...collection,
                slug: 'user-exports',
                labels: {
                  singular: 'User Export',
                  plural: 'User Exports',
                },
                access: {
                  // Only super admins can access user exports
                  read: ({ req }) => req.user?.role === 'superadmin',
                  create: ({ req }) => req.user?.role === 'superadmin',
                },
              }
            },
          },
          import: {
            // Create a separate 'user-imports' collection
            overrideCollection: ({ collection }) => {
              return {
                ...collection,
                slug: 'user-imports',
                labels: {
                  singular: 'User Import',
                  plural: 'User Imports',
                },
                access: {
                  read: ({ req }) => req.user?.role === 'superadmin',
                  create: ({ req }) => req.user?.role === 'superadmin',
                },
              }
            },
          },
        },
        {
          slug: 'pages',
          // Pages will use the default 'exports' and 'imports' collections
        },
        {
          slug: 'posts',
          // Posts will also use the default collections
        },
      ],
    }),
  ],
})

In this example:

  • User exports are stored in user-exports collection with restricted access
  • User imports are tracked in user-imports collection
  • Pages and posts share the default exports and imports collections

Combining Top-Level and Per-Collection Overrides

You can combine the top-level overrideExportCollection / overrideImportCollection functions with per-collection overrides. The top-level override is applied first, then the per-collection override:

importExportPlugin({
  // Apply to ALL export collections (both default and custom slugs)
  overrideExportCollection: ({ collection }) => {
    return {
      ...collection,
      admin: {
        ...collection.admin,
        group: 'Data Management',
      },
    }
  },

  collections: [
    {
      slug: 'sensitive-data',
      export: {
        // This override is applied AFTER the top-level override
        overrideCollection: ({ collection }) => {
          return {
            ...collection,
            slug: 'sensitive-exports',
            access: {
              read: () => false, // Completely restrict read access
              create: ({ req }) => req.user?.role === 'admin',
            },
          }
        },
      },
    },
  ],
})

Hooks

Collection-level hooks let you intercept each batch of documents before it is written (to file on export, or to the database on import) and after it has been written. Hooks fire once per batch and work for both csv and json formats.

Hook arguments

All hooks receive:

Argument Type Description
batchNumber number Current batch, starting at 1.
data Record<string, unknown>[] Transformed batch — flat rows for CSV export, nested docs otherwise.
format string Export/import format. Open-ended ('csv' | 'json' | string).
originalData array Source data before transformation. Read-only — do not mutate.
req PayloadRequest The full request object.
totalBatches number Total number of batches in this operation.

ImportAfterHook additionally receives result: ImportResult (per-batch counts and errors) instead of data/originalData.

before hooks — modify data

before hooks return the (optionally modified) data array, which replaces the batch going into the write step. Returning an empty array skips the write for that batch without aborting remaining batches.

import { importExportPlugin } from '@payloadcms/plugin-import-export'

importExportPlugin({
  collections: [
    {
      slug: 'users',
      export: {
        hooks: {
          // Mask sensitive fields before the batch is written to file
          before: ({ data, format }) => {
            return data.map((row) => {
              const { passwordHash: _ph, ssn: _ssn, ...safe } = row
              return safe
            })
          },
        },
      },
      import: {
        hooks: {
          // Normalise incoming data before it is written to the database
          before: ({ data }) => {
            return data.map((doc) => ({
              ...doc,
              email:
                typeof doc.email === 'string'
                  ? doc.email.toLowerCase()
                  : doc.email,
            }))
          },
        },
      },
    },
  ],
})

after hooks — logging and observability

after hooks fire after the write has completed. Their return value is ignored — use them for logging, metrics, or notifications.

importExportPlugin({
  collections: [
    {
      slug: 'orders',
      export: {
        hooks: {
          after: ({ batchNumber, totalBatches, data, format, req }) => {
            req.payload.logger.info({
              msg: `Export batch ${batchNumber}/${totalBatches} written`,
              format,
              docsInBatch: data.length,
            })
          },
        },
      },
      import: {
        hooks: {
          after: ({ batchNumber, totalBatches, result, req }) => {
            req.payload.logger.info({
              msg: `Import batch ${batchNumber}/${totalBatches} complete`,
              imported: result.imported,
              updated: result.updated,
              errors: result.errors.length,
            })
          },
        },
      },
    },
  ],
})

Using originalData for context

originalData gives you the raw source before any transformation. For exports it is the original DB documents; for imports it is the raw parsed file rows. This is useful when you need context from the full document while building modified flat rows.

export: {
  hooks: {
    before: ({ data, originalData }) => {
      return data.map((row, i) => ({
        ...row,
        // Add a computed column not present in the DB document
        displayName: `${originalData[i]?.firstName} ${originalData[i]?.lastName}`,
      }))
    },
  },
},

Column name mapping for foreign systems

When importing from or exporting to a foreign system, column names rarely match your Payload field names. Use the batch before hooks to rename keys on the way in or out.

Exporting with renamed CSV columns

Use export.hooks.before to rewrite every row's keys to the foreign system's column names. The returned shape is what gets written to the file.

import { importExportPlugin } from '@payloadcms/plugin-import-export'

const exportRenameMap: Record<string, string> = {
  title: 'Post Title',
  excerpt: 'Summary',
  count: 'View Count',
}

importExportPlugin({
  collections: [
    {
      slug: 'posts',
      export: {
        hooks: {
          before: ({ data }) =>
            data.map((row) => {
              const renamed: Record<string, unknown> = {}
              for (const [key, value] of Object.entries(row)) {
                renamed[exportRenameMap[key] ?? key] = value
              }
              return renamed
            }),
        },
      },
    },
  ],
})

JSON exports work the same way — the hook receives the fully-transformed batch, and the returned keys become the top-level JSON keys.

Importing from foreign CSV columns

Use import.hooks.before to rewrite incoming keys back to Payload field names. The hook receives already-unflattened documents, so foreign top-level keys like Post Title are still present and can be remapped to title before the DB write. Keys you don't include in the returned object are dropped.

const importRenameMap: Record<string, string> = {
  'Post Title': 'title',
  Summary: 'excerpt',
  'View Count': 'count',
}

importExportPlugin({
  collections: [
    {
      slug: 'posts',
      import: {
        hooks: {
          before: ({ data }) =>
            data.map((doc) => {
              const renamed: Record<string, unknown> = {}
              for (const [key, value] of Object.entries(doc)) {
                const payloadKey = importRenameMap[key]
                if (payloadKey) {
                  renamed[payloadKey] = value
                }
                // Foreign keys not present in the rename map are dropped.
              }
              return renamed
            }),
        },
      },
    },
  ],
})

CSV cell values arrive as strings. Payload coerces basic scalar types automatically on write, but if a target field has stricter validation you can coerce inside the hook (renamed.count = Number(value)).

Field-level export rename for shared fields

When a field definition is shared across collections and the rename should travel with the field, use the field's own hooks.beforeExport. Mutate siblingData to add the renamed key and return undefined so the original field key is dropped from the output:

import type { FieldBeforeExportHook } from '@payloadcms/plugin-import-export'

const sharedNameField = {
  name: 'sharedName',
  type: 'text' as const,
  custom: {
    'plugin-import-export': {
      hooks: {
        beforeExport: (({ siblingData, value }) => {
          siblingData['Display Name'] = value
          return undefined
        }) satisfies FieldBeforeExportHook,
      },
    },
  },
}

This pattern is export-only. Field-level hooks.beforeImport does not fire for foreign columns — it's keyed by the incoming column name, so it can't pick up a column whose name doesn't already match a Payload field. For the reverse direction, use the collection-level import.hooks.before recipe above.

Field Options

In addition to collection-level hooks, you can configure field-level export and import behavior directly on any field's custom['plugin-import-export'] config. This is especially useful when:

  • You share a field definition across multiple collections and want the behavior to travel with the field
  • You need to transform a deeply nested field without navigating the full document structure in a collection-level hook
Property Type Description
disabled boolean When true the field is completely excluded from the import-export plugin.
hooks.beforeExport function Runs before a field value is exported. Works for CSV and JSON.
hooks.beforeImport function Runs before a field value is imported. Works for CSV and JSON.

Disabling Fields

To completely exclude a field from import and export operations:

{
  name: 'internalField',
  type: 'text',
  custom: {
    'plugin-import-export': {
      disabled: true,
    },
  },
}

When a field is disabled:

  • It will not appear in export CSV/JSON files
  • It will be ignored during import operations
  • Nested fields inside disabled parent fields are also excluded

hooks.beforeExport

Use hooks.beforeExport to transform a field's value before it is exported. Works for both csv and json formats.

The beforeExport function receives:

Property Type Description
columnName string Column/field path, underscore-separated for nested fields (includes array indices).
data object The top-level document being exported.
format string 'csv' or 'json'.
siblingData object Writable output at the current level. CSV: the flat row accumulator. JSON: the sibling output object.
siblingDoc object Read-only source at the current level, before any transformation. Use this to read another sibling's raw value.
value unknown The field value from the source document.

Return a value to replace the field, undefined to use default behavior, or mutate siblingData to add extra columns at the same level.

Example — splitting a relationship into multiple columns:

import type { FieldBeforeExportHook } from '@payloadcms/plugin-import-export'

const authorFieldWithHooks = {
  name: 'author',
  type: 'relationship',
  relationTo: 'users',
  custom: {
    'plugin-import-export': {
      hooks: {
        beforeExport: (({ value, columnName, siblingData, format }) => {
          if (
            format === 'csv' &&
            value &&
            typeof value === 'object' &&
            'id' in value &&
            'email' in value
          ) {
            siblingData[`${columnName}_id`] = (
              value as { id: number | string }
            ).id
            siblingData[`${columnName}_email`] = (
              value as { email: string }
            ).email
            return undefined
          }
          return value
        }) satisfies FieldBeforeExportHook,
      },
    },
  },
}

Because the hook is defined on the field itself, you can share authorFieldWithHooks across multiple collections and each will get the same export behavior automatically.

hooks.beforeImport

Use hooks.beforeImport to transform a field's value before it is imported. Works for both csv and json formats.

The beforeImport function receives:

Property Type Description
columnName string Column/field path.
data object The full flat row (CSV) or top-level parsed document (JSON) being imported.
format string 'csv' or 'json'.
siblingData object Data at the current level. CSV: same reference as data. JSON: the parent-level object.
siblingDoc object Read-only source at the current level, before any transformation. CSV: same as data.
value unknown The raw value from the import file.

Return the transformed value, undefined to skip, or null to explicitly set null.

import type { FieldBeforeImportHook } from '@payloadcms/plugin-import-export'

{
  name: 'author',
  type: 'relationship',
  relationTo: 'users',
  custom: {
    'plugin-import-export': {
      hooks: {
        beforeImport: (({ value, columnName, data, format }) => {
          if (format === 'csv') {
            const id = data[`${columnName}_id`]
            return id ?? undefined
          }
          return value
        }) satisfies FieldBeforeImportHook,
      },
    },
  },
}

Execution Order

Field-level hooks run before collection-level hooks:

  1. Field-level hooks.beforeExport/hooks.beforeImport: run per-field, per-document, during transformation
  2. Collection-level export.hooks.before/import.hooks.before: run per-batch, on the already-field-transformed data

Virtual Fields

Virtual fields (fields with virtual: true) are handled differently during import and export:

  • Export: Virtual fields ARE included in exports. They contain computed values from hooks.
  • Import: Virtual fields are SKIPPED during import. Since they're computed, they cannot be imported.

Exporting Data

There are four possible ways that the plugin allows for exporting documents, the first two are available in the admin UI from the list view of a collection:

  1. Direct download - Using a POST to /api/exports/download and streams the response as a file download
  2. File storage - Goes to the exports collection as an uploads enabled collection
  3. Local API - A create call to the exports collection: payload.create({ collection: 'exports', data: { collectionSlug: 'pages', format: 'json' } })
  4. Jobs Queue - payload.jobs.queue({ task: 'createCollectionExport', input: parameters })

In the Export drawer you can choose:

  • Download — Streams the export file directly to the browser without saving. Nothing is stored in the exports collection.
  • Save — Creates a document in the exports collection (an upload) so the export is stored and can be reused or downloaded later.

To download a saved export, open the exports collection (go to /admin/collections/exports or, if you made it visible as in Collection Visibility, use the sidebar), open the export document, and use the file download from there. Either the Download or Save option can be disabled per collection via ExportConfig (disableDownload, disableSave).

The UI for creating exports provides options so that users can be selective about which documents to include and also which columns or fields to include.

Selection Modes

When opening the export drawer from a collection's list view, you can choose which documents to export:

Mode Description
Use all documents Export the entire collection (respects any limit you set)
Use current filters Export only documents matching your active list view filters
Use current selection Export only the documents you've checked in the list view

It is necessary to add access control to the uploads collection configuration using the overrideExportCollection function if you have enabled this plugin on collections with data that some authenticated users should not have access to.

**Note**: Users who have read access to the upload collection may be able to download data that is normally not readable due to [access control](../access-control/overview).

The following parameters are used by the export function to handle requests:

Property Type Description
format string Either csv or json to determine the shape of data exported
limit number The max number of documents to export. Leave empty to export all matching documents.
page number The page of documents to start from (used with limit for pagination).
sort string The field to use for ordering documents (e.g., createdAt, -title for descending).
depth number How deeply to populate relationships. Default: 1. Set to 0 to export IDs only.
locale string The locale code to query documents or all for multi-locale export.
drafts boolean When true, includes draft versions for collections with drafts enabled.
fields string[] Which collection fields to include in the export. Defaults to all fields.
collectionSlug string The collection slug to export from.
where object The WhereObject used to query documents to export. This is set by making selections or filters from the list view
filename string The name for the exported file (without extension).

Importing Data

The plugin allows importing data from CSV or JSON files. There are several ways to import:

  1. Admin UI - Use the Import drawer from the list view of a collection
  2. File storage - Create an import document in the imports collection with an uploaded file
  3. Local API - Create an import document: payload.create({ collection: 'imports', data: { collectionSlug: 'pages', importMode: 'create' }, file: { ... } })
  4. Jobs Queue - payload.jobs.queue({ task: 'createCollectionImport', input: parameters })

Import Parameters

Property Type Description
collectionSlug string The collection to import into.
importMode string create, update, or upsert (default: create).
matchField string The field to use for matching existing documents during update or upsert operations.
locale string The locale to use for localized fields.

The matchField option allows you to match documents by a field other than id. For example, if importing users, you could match by email instead of id:

payload.create({
  collection: 'imports',
  data: {
    collectionSlug: 'users',
    importMode: 'upsert',
    matchField: 'email',
  },
  file: csvFile,
})

Import Modes

Mode Description
create Only creates new documents. Documents with existing IDs will fail.
update Only updates existing documents. Requires id column in CSV. Documents without matching IDs will fail.
upsert Creates new documents or updates existing ones based on id. Most flexible option.

Import Results

After an import completes, the import document is updated with a summary:

Property Type Description
status string pending, processing, completed, or failed
summary.total number Total number of rows processed
summary.imported number Number of successfully imported documents
summary.updated number Number of updated documents (update/upsert modes)
summary.issues number Number of rows that failed
summary.issueDetails array Details about each failure

CSV Format

Column Naming Convention

CSV columns use underscore (_) notation to represent nested fields:

Field Path CSV Column Name
title title
group.value group_value
array[0].field array_0_field
blocks[0] blocks_0_<blockSlug>_blockType
localized (en) localized_en

Relationship Columns

For relationship fields, the column format varies based on the relationship type:

Relationship Type Column(s)
hasOne (monomorphic) fieldName
hasOne (polymorphic) fieldName_relationTo, fieldName_id
hasMany (monomorphic) fieldName_0, fieldName_1, etc.
hasMany (polymorphic) fieldName_0_relationTo, fieldName_0_id

Value Handling

During CSV import, certain values are automatically converted:

CSV Value Converted To Notes
true, TRUE true (boolean) Case-insensitive
false, FALSE false (boolean) Case-insensitive
null, NULL null Use hooks.beforeImport to preserve as string
Empty string '' or omitted Depends on field type
Numeric strings number Auto-detected for integers and floats

To preserve literal strings like "null" or "true", use hooks.beforeImport:

{
  name: 'specialField',
  type: 'text',
  custom: {
    'plugin-import-export': {
      hooks: {
        beforeImport: ({ value }) => {
          // Return raw value without automatic conversion
          return value
        },
      },
    },
  },
}

Localized Fields

Single Locale Export

When exporting with a specific locale selected, localized fields appear without a locale suffix:

title,description
"English Title","English Description"

Multi-Locale Export

When exporting with locale set to all, each localized field gets a column per configured locale:

title_en,title_es,title_de,description_en,description_es,description_de
"English","Español","Deutsch","Desc EN","Desc ES","Desc DE"

Importing Localized Fields

For single-locale import, data goes into the locale specified in the import settings:

title,description
"New Title","New Description"

For multi-locale import, use locale suffixes in column names to import multiple locales at once:

title_en,title_es
"English Title","Título en Español"

JSON Format

When using JSON format for import/export:

  • Export: Documents are exported as a JSON array, preserving their nested structure
  • Import: Expects a JSON array of document objects

JSON format preserves the exact structure of your data, including:

  • Nested objects and arrays
  • Rich text (Lexical) structures with numeric properties
  • Relationship references
  • All field types in their native format

Example JSON export:

[
  {
    "id": "abc123",
    "title": "My Page",
    "group": {
      "value": "nested value",
      "array": [{ "field1": "item 1" }, { "field2": "item 2" }]
    },
    "blocks": [
      {
        "blockType": "hero",
        "title": "Hero Title"
      }
    ]
  }
]