n8n Data Transformation: How to Reshape Any Data Structure
n8n Data Transformation: How to Reshape Any Data Structure
• Logic Workflow Team

n8n Data Transformation: How to Reshape Any Data Structure

#n8n #data transformation #JSON #tutorial #automation #API

Your workflow just crashed because the API returned an array when you expected an object. Or maybe a single item arrived when you needed multiple. Perhaps the field names use snake_case but your destination requires camelCase. These data structure mismatches cause most n8n workflow failures.

Data transformation sits at the heart of every useful automation. You pull data from one system, reshape it to match another system’s requirements, and push it forward. This sounds simple until you realize that every API speaks its own dialect of JSON, and n8n has specific structural requirements of its own.

The Data Structure Problem

APIs don’t care about your workflow. Shopify returns orders with nested line item arrays. HubSpot expects flat contact records. Google Sheets outputs rows as individual objects. Stripe sends webhook payloads wrapped in event objects. None of these match each other, and none arrive in exactly the format your next node expects.

n8n requires a specific data structure: an array of objects, each containing a json property. When incoming data doesn’t match this format, you get cryptic errors like “Cannot read properties of undefined” or silent failures where data simply disappears between nodes.

Most n8n users spend more time wrestling with data transformation than any other aspect of workflow building. The n8n community forums overflow with questions about restructuring JSON, splitting arrays, and handling nested objects.

What You’ll Learn

This guide covers everything you need to transform data confidently in n8n:

  • n8n’s data structure requirements and why they matter
  • Built-in transformation nodes (Set, Split Out, Aggregate, Merge) and when to use each
  • 80+ data transformation functions built into n8n expressions
  • Code node patterns for complex transformations that visual nodes can’t handle
  • Real-world scenarios from API integrations, e-commerce, and data cleanup
  • Troubleshooting techniques for common transformation errors

By the end, you’ll know exactly which tool to reach for when data arrives in the wrong shape.

Understanding n8n’s Data Structure

Before transforming data, you need to understand what n8n expects. This foundation prevents 90% of transformation headaches.

Items, JSON, and the Required Format

n8n workflows process items. Each item is an object containing at minimum a json property with your actual data. When you see output from any node, you’re looking at an array of items.

The required format:

[
  {
    json: {
      name: "John",
      email: "john@example.com"
    }
  },
  {
    json: {
      name: "Jane",
      email: "jane@example.com"
    }
  }
]

Notice the outer array, the individual objects, and the json wrapper around each record. This structure isn’t optional. Nodes that produce data in other formats cause downstream failures.

How Data Flows Between Nodes

Each node receives items from the previous node, processes them, and outputs new items to the next node. The transformation happens at each step.

InputNodeOutput
1 item with arraySplit OutMultiple items (one per array element)
Multiple itemsAggregate1 item with combined data
Multiple itemsSetSame number of items, modified fields
2 input streamsMergeCombined items based on mode

Understanding this flow helps you choose the right transformation approach.

Common Structure Mistakes

Mistake 1: Returning raw data from Code nodes

// Wrong - returns raw object
return { name: "John" };

// Correct - returns array of items
return [{ json: { name: "John" } }];

Mistake 2: Accessing arrays without splitting

If a field contains an array, you can’t directly use each element in subsequent nodes. You need to split the array into separate items first using the Split Out node.

Mistake 3: Ignoring nested structures

{{ $json.user.address.city }} fails silently if user or address is undefined. Use optional chaining: {{ $json.user?.address?.city }}.

For expression-specific patterns, see our complete n8n expressions cheatsheet.

Built-in Transformation Nodes

n8n provides visual nodes for common transformation tasks. These should be your first choice before reaching for the Code node.

The Set Node (Edit Fields)

The Set node (also called Edit Fields) is the workhorse of data transformation. Use it to:

  • Add new fields with static or dynamic values
  • Rename existing fields
  • Remove unwanted fields
  • Transform values using expressions

Adding a calculated field:

Configure the Set node with a new field called fullName and expression {{ $json.firstName }} {{ $json.lastName }}.

Renaming fields:

Switch to “Manual Mapping” mode. Map first_name to firstName, last_name to lastName, and so on.

Removing fields:

Enable “Only Keep Fields Set in This Node” to output only the fields you explicitly define.

Tip: The Set node handles most simple transformations. Before using the Code node, ask yourself if Set node expressions can accomplish the same result.

Split Out Node

The Split Out node converts a single item containing an array into multiple separate items. This is essential when APIs return list data.

When to use:

  • An API returns { orders: [{...}, {...}, {...}] } and you need to process each order
  • A webhook payload contains a line_items array
  • Any time you have one item with an array field that needs individual processing

Configuration:

  1. Set “Field to Split Out” to the array field name (e.g., orders or line_items)
  2. Optionally include parent fields in each output item

Example input:

{
  "orderId": "12345",
  "items": [
    { "product": "Widget", "qty": 2 },
    { "product": "Gadget", "qty": 1 }
  ]
}

After Split Out on items:

[
  { "product": "Widget", "qty": 2, "orderId": "12345" },
  { "product": "Gadget", "qty": 1, "orderId": "12345" }
]

Each array element becomes a separate item, optionally carrying parent field values.

Aggregate Node

The Aggregate node does the opposite of Split Out. It takes multiple items and combines them into fewer items (typically one).

When to use:

  • You processed items individually and need to recombine them
  • Building a summary from multiple records
  • Creating a payload that requires an array of objects

Aggregation modes:

ModeWhat It DoesResult
Aggregate Individual FieldsCollects values from a specific field across all itemsSingle item with array field
Aggregate All Item DataCombines all items into an arraySingle item with all data as array

Example: After processing order line items separately, use Aggregate to reconstruct the order with the modified items:

// Input: 3 separate items (one per line item)
// Output: 1 item with { lineItems: [...all three items...] }

Other Transformation Nodes

Merge Node

Combines data from two input connections. Multiple modes handle different scenarios:

  • Append: Add items from both inputs together
  • Combine: Match items by position or field value
  • Multiplex: Create all possible combinations

Use Merge when you need to enrich data from one source with data from another.

Sort Node

Reorders items based on field values. Supports ascending, descending, and custom sort expressions.

Limit Node

Restricts output to a maximum number of items. Useful for testing with large datasets or implementing pagination.

Compare Datasets Node

Identifies differences between two data streams. Outputs items that are new, updated, unchanged, or removed.

Filter Node vs IF Node

  • Filter: Removes items that don’t match criteria (reduce item count)
  • IF: Routes ALL items to different branches based on criteria (item count unchanged)

For workflow architecture guidance, check our n8n workflow best practices guide.

Data Transformation Functions

n8n includes over 80 built-in functions for transforming data within expressions. These work anywhere you use the expression editor.

String Transformation Functions

FunctionDescriptionExample
.extractEmail()Extract email from text{{ "Contact me at john@test.com".extractEmail() }}
.extractUrl()Extract URL from text{{ "Visit https://example.com today".extractUrl() }}
.extractDomain()Get domain from URL{{ "https://sub.example.com/path".extractDomain() }}
.isEmail()Check if valid email{{ "test@example.com".isEmail() }} returns true
.isEmpty()Check if empty/null{{ "".isEmpty() }} returns true
.base64Encode()Encode as base64{{ "hello".base64Encode() }}
.base64Decode()Decode from base64{{ "aGVsbG8=".base64Decode() }}
.toTitleCase()Title Case Text{{ "hello world".toTitleCase() }} returns Hello World
.toSnakeCase()snake_case_text{{ "Hello World".toSnakeCase() }} returns hello_world
.hash('sha256')Generate hash{{ "text".hash("sha256") }}

Practical example - clean and validate email:

{{ $json.userInput.trim().toLowerCase().isEmail() ? $json.userInput.trim().toLowerCase() : null }}

Array Transformation Functions

FunctionDescriptionExample
.removeDuplicates()Remove duplicate values{{ $json.tags.removeDuplicates() }}
.removeDuplicates('email')Remove duplicates by key{{ $json.users.removeDuplicates("email") }}
.pluck('name')Extract single property{{ $json.users.pluck("name") }} returns array of names
.sort()Sort array{{ $json.numbers.sort() }}
.sort('lastName')Sort by property{{ $json.users.sort("lastName") }}
.first()Get first element{{ $json.items.first() }}
.last()Get last element{{ $json.items.last() }}
.unique()Get unique values{{ $json.categories.unique() }}

Practical example - extract all email domains:

{{ $json.contacts.pluck("email").map(e => e.extractDomain()) }}

Object Transformation Functions

FunctionDescriptionExample
.isEmpty()Check if object is empty{{ $json.metadata.isEmpty() }}
.keys()Get object keys{{ Object.keys($json.data) }}
.values()Get object values{{ Object.values($json.data) }}
.merge(obj)Merge two objects{{ $json.defaults.merge($json.overrides) }}

Using JMESPath for Complex Queries

When standard expressions become unwieldy, JMESPath provides powerful JSON querying.

Basic syntax:

{{ $jmespath($json, "query") }}

Examples:

// Get all order totals
{{ $jmespath($json, "orders[*].total") }}

// Filter orders over $100
{{ $jmespath($json, "orders[?total > `100`]") }}

// Get first and last names as pairs
{{ $jmespath($json.people, "[].[firstName, lastName]") }}

// Nested extraction
{{ $jmespath($json, "response.data.users[*].{name: name, email: email}") }}

JMESPath shines when you need to filter, project, or reshape deeply nested structures without Code nodes.

Code Node Patterns for Data Transformation

Sometimes visual nodes and expressions can’t handle your transformation. The Code node provides full JavaScript capabilities for these cases.

When to use the Code node: If your transformation requires multiple steps, loops with conditions, or complex restructuring that would need 5+ visual nodes chained together.

For comprehensive Code node recipes, see our n8n Code Node JavaScript guide.

Restructuring Nested JSON

Problem: API returns deeply nested data you need to flatten.

const items = $input.all();

return items.map(item => {
  const data = item.json;

  return {
    json: {
      // Flatten nested user data
      userId: data.user?.id,
      userName: `${data.user?.profile?.firstName ?? ''} ${data.user?.profile?.lastName ?? ''}`.trim(),
      userEmail: data.user?.contact?.email?.toLowerCase(),
      userCity: data.user?.contact?.address?.city,

      // Keep some original fields
      createdAt: data.createdAt,
      status: data.status
    }
  };
});

Key patterns:

  • Optional chaining (?.) prevents undefined errors
  • Nullish coalescing (??) provides fallbacks
  • Template literals combine fields

Splitting Arrays into Items

Problem: Single item with array needs to become multiple items.

const items = $input.all();
const sourceArray = items[0].json.records || [];

// Convert each array element to an n8n item
return sourceArray.map(record => ({
  json: {
    ...record,
    processedAt: new Date().toISOString()
  }
}));

Alternative with parent data preserved:

const items = $input.all();
const parent = items[0].json;
const children = parent.lineItems || [];

return children.map(child => ({
  json: {
    orderId: parent.orderId,
    orderDate: parent.orderDate,
    ...child
  }
}));

Aggregating Items into Single Output

Problem: Multiple items need to become one combined item.

const items = $input.all();

// Collect all records into one array
const allRecords = items.map(item => item.json);

// Calculate summary
const totalValue = allRecords.reduce((sum, r) => sum + (r.amount || 0), 0);

return [{
  json: {
    records: allRecords,
    count: allRecords.length,
    totalValue: totalValue.toFixed(2),
    processedAt: new Date().toISOString()
  }
}];

Handling API Response Variations

Problem: API sometimes returns array, sometimes single object.

const items = $input.all();
const response = items[0].json;

// Normalize to always be an array
let records = response.data;
if (!Array.isArray(records)) {
  records = records ? [records] : [];
}

// Now safely process as array
return records.map(record => ({
  json: {
    ...record,
    _normalized: true
  }
}));

For error handling patterns, see our timeout troubleshooting guide.

Real-World Transformation Scenarios

Theory is useful, but real-world examples show how these techniques combine.

API Response to CRM-Ready Format

Scenario: Transform webhook data for HubSpot or Salesforce import.

Input from form submission:

{
  "submission_id": "12345",
  "form_data": {
    "first_name": "John",
    "last_name": "Smith",
    "work_email": "john@acme.com",
    "company_name": "Acme Corp",
    "phone_number": "+1-555-0123"
  },
  "submitted_at": "2025-01-15T10:30:00Z"
}

Transformation using Set node:

FieldExpression
firstname{{ $json.form_data.first_name }}
lastname{{ $json.form_data.last_name }}
email{{ $json.form_data.work_email.toLowerCase() }}
company{{ $json.form_data.company_name }}
phone{{ $json.form_data.phone_number.replace(/-/g, '') }}
lead_sourceWebsite Form
created_date{{ $json.submitted_at }}

E-commerce Order Processing

Scenario: Shopify order needs processing per line item, then reconstitution.

Workflow pattern:

  1. Webhook receives order with line items array
  2. Split Out on line_items field - creates item per product
  3. HTTP Request to inventory API for each item
  4. Set calculates fulfillment data
  5. Aggregate recombines into single order
  6. HTTP Request sends to fulfillment system

Split Out configuration:

  • Field to Split Out: line_items
  • Include Other Fields: Enable (keeps order_id, customer, etc.)

Aggregate configuration:

  • Aggregate: Individual Fields
  • Field Name: line_items
  • Output Field Name: processed_items

Spreadsheet Data Cleanup

Scenario: Google Sheets data has inconsistent formatting.

const items = $input.all();

return items
  // Remove empty rows
  .filter(item => {
    const data = item.json;
    return data.email && data.email.trim().length > 0;
  })
  // Clean and normalize each row
  .map(item => {
    const data = item.json;

    return {
      json: {
        email: data.email?.trim().toLowerCase(),
        name: (data.name || data.Name || '').trim(),
        phone: (data.phone || data.Phone || '')
          .replace(/[^\d+]/g, ''), // Keep only digits and +
        status: (data.status || 'unknown').toLowerCase(),
        // Convert empty strings to null
        notes: data.notes?.trim() || null
      }
    };
  });

Multi-API Data Merging

Scenario: Combine customer data from CRM with order data from e-commerce.

Workflow pattern:

  1. HTTP Request to CRM API - get customers
  2. HTTP Request to Shopify API - get orders (parallel)
  3. Merge node in Combine mode:
    • Merge Mode: Combine
    • Combine by: Fields
    • Field: email (from both sources)

Post-merge Set node:

// Expression for enriched customer record
{
  email: {{ $json.email }},
  name: {{ $json.crmData?.name ?? $json.shopifyData?.customer_name }},
  totalOrders: {{ $json.shopifyData?.order_count ?? 0 }},
  lifetimeValue: {{ $json.shopifyData?.total_spent ?? 0 }},
  segment: {{ $json.crmData?.segment ?? 'Unknown' }}
}

Troubleshooting Data Transformation Issues

When transformations fail, these techniques help identify and fix the problem.

Common Errors and Solutions

ErrorCauseFix
”Cannot read properties of undefined”Accessing property on null/undefined valueUse optional chaining: $json.user?.email
”items.map is not a function”Expected array but received objectNormalize to array: Array.isArray(data) ? data : [data]
”[object Object]” in outputExpression returns object instead of stringUse JSON.stringify() or access specific property
”The JSON Output contains invalid JSON”Malformed JSON in Set nodeUse JSON fixer tool
Empty output from Split OutWrong field name or field is emptyCheck exact field name, verify array exists
Duplicate items after MergeIncorrect merge mode or fieldReview merge configuration and input data

Debugging Data Flow

Step 1: Pin data for testing

Click on any node’s output and select “Pin Data” to freeze it. This lets you test downstream nodes with consistent input.

Step 2: Check actual structure

In the Code node, log the full structure:

const items = $input.all();
console.log('Items count:', items.length);
console.log('First item:', JSON.stringify(items[0], null, 2));
return items;

Check browser developer console (F12) for output.

Step 3: Trace the transformation

Add temporary Set nodes between steps to see exactly what data looks like at each stage. Remove them once debugging is complete.

Step 4: Use the debugger tool

Our free n8n workflow debugger analyzes error messages and provides specific fixes.

Performance Considerations

Large datasets:

  • Use Split In Batches to process in chunks
  • Avoid loading entire datasets into Code node variables
  • Consider pagination for API requests

Memory optimization:

  • Filter early in the workflow to reduce item count
  • Don’t carry unnecessary fields through multiple nodes
  • Use streaming for file processing when available

Code node efficiency:

// Less efficient - creates intermediate arrays
const result = items
  .filter(i => i.json.active)
  .map(i => i.json)
  .filter(j => j.value > 100);

// More efficient - single pass
const result = [];
for (const item of items) {
  if (item.json.active && item.json.value > 100) {
    result.push({ json: item.json });
  }
}
return result;

For scaling challenges, our queue mode guide covers distributed processing.

Frequently Asked Questions

How do I access nested data in n8n expressions?

Use dot notation for known structures and optional chaining for uncertain ones.

Dot notation:

{{ $json.user.address.city }}

Bracket notation (for special characters):

{{ $json['user-data']['street-address'] }}

Optional chaining (safe access):

{{ $json.user?.address?.city ?? 'Unknown' }}

The optional chaining operator (?.) prevents errors when intermediate properties are undefined. The nullish coalescing operator (??) provides a fallback value.

For arrays, use index access:

{{ $json.orders[0].total }}  // First order
{{ $json.orders?.at(-1)?.total }}  // Last order (safe)

What’s the difference between Split Out and the Code node for splitting arrays?

Split Out node:

  • Visual, no code required
  • Handles standard array splitting
  • Automatically includes parent fields if configured
  • Best for simple array-to-items conversion

Code node:

  • Full control over output structure
  • Can apply transformations while splitting
  • Better for conditional splitting or complex restructuring
  • Required when you need to calculate values during split

Decision framework:

ScenarioUse Split OutUse Code
Simple array to itemsYesNo
Array with conditional filteringNoYes
Need to transform each elementSometimesYes
Complex parent-child data mergingNoYes
Quick prototypingYesNo

Start with Split Out. Move to Code node when you need capabilities Split Out doesn’t offer.


Why does my Set node output show “[object Object]”?

This happens when an expression returns an object instead of a string, and n8n converts it to text for display.

Common causes:

  1. Accessing an object, not a property:
// Returns object - shows [object Object]
{{ $json.user }}

// Returns string - shows actual value
{{ $json.user.name }}
  1. Array access without index:
// Returns array object
{{ $json.tags }}

// Returns specific value
{{ $json.tags[0] }}

// Returns comma-separated string
{{ $json.tags.join(', ') }}

Fixes:

  • Access the specific property you need
  • Use JSON.stringify() if you need the full object as text
  • Use .join() for arrays
  • Use our expression validator to test expressions

How do I handle API responses that sometimes return an array and sometimes a single object?

Normalize the data at the start of your workflow:

In Set node (using expression):

{{ Array.isArray($json.data) ? $json.data : [$json.data] }}

In Code node (more control):

const items = $input.all();
const response = items[0].json;

let records = response.data;

// Normalize to array
if (!records) {
  records = [];
} else if (!Array.isArray(records)) {
  records = [records];
}

return records.map(record => ({ json: record }));

This pattern ensures downstream nodes always receive consistent input regardless of API behavior.


Can I transform data without using the Code node?

Yes. For most transformations, visual nodes and expressions are sufficient.

Transformations possible without Code node:

TaskSolution
Rename fieldsSet node with field mapping
Add calculated fieldsSet node with expressions
Filter itemsFilter node or IF node
Split array to itemsSplit Out node
Combine itemsAggregate node
Merge data streamsMerge node
Sort itemsSort node
Remove duplicatesExpression with .removeDuplicates()
Extract nested valuesExpression with dot notation
Date formattingExpression with Luxon methods
String manipulationExpression with transformation functions

When Code node becomes necessary:

  • Complex conditional logic across multiple fields
  • Custom aggregations (group by, complex sums)
  • Restructuring that requires loops
  • When 5+ visual nodes would be needed for the same task

The n8n documentation provides additional examples of visual transformation techniques.

Choosing Your Transformation Approach

Not every situation requires the same tool. Use this decision framework:

Start with expressions when:

  • Accessing or formatting single values
  • Simple calculations
  • Quick field transformations

Use Set node when:

  • Adding, renaming, or removing fields
  • Multiple field transformations on same item
  • Need visual clarity in workflow

Use Split Out/Aggregate when:

  • Converting between array and items structure
  • Standard split or combine operations

Use Code node when:

  • Complex conditional logic
  • Custom aggregations or grouping
  • Performance critical with large datasets
  • Visual approach would require 5+ nodes

For credentials handling across transformations, see our credential management guide.

Next Steps

You now have the knowledge to handle virtually any data transformation in n8n. Bookmark this page for reference when you encounter new transformation challenges.

Continue learning:

If your workflows need professional optimization or you’re stuck on complex transformations, our n8n consulting services can help you build reliable, maintainable automations.

Ready to Automate Your Business?

Tell us what you need automated. We'll build it, test it, and deploy it fast.

âś“ 48-72 Hour Turnaround
âś“ Production Ready
âś“ Free Consultation
⚡

Create Your Free Account

Sign up once, use all tools free forever. We require accounts to prevent abuse and keep our tools running for everyone.

or

You're in!

Check your email for next steps.

By signing up, you agree to our Terms of Service and Privacy Policy. No spam, unsubscribe anytime.

🚀

Get Expert Help

Add your email and one of our n8n experts will reach out to help with your automation needs.

or

We'll be in touch!

One of our experts will reach out soon.

By submitting, you agree to our Terms of Service and Privacy Policy. No spam, unsubscribe anytime.