# How to Handle Nested Data in Retool

- Tool: Retool
- Difficulty: Intermediate
- Time required: 20-25 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## TL;DR

Access nested JSON data in Retool using dot notation: {{ query1.data[0].profile.email }}. For tables, flatten nested structures in a transformer before binding to {{ transformer1.value }}. Use optional chaining (?.) to avoid errors on missing fields. The JSON Explorer component helps you visualize and navigate complex response structures.

## Working with Nested JSON Data from REST APIs and Complex SQL Results

REST APIs often return deeply nested JSON objects — user profiles inside a user object, addresses as nested arrays, metadata as key-value maps. Retool handles nested data well, but requires explicit dot-notation access and often some transformer-based flattening before the data is ready to display in tables or bind to form fields.

This tutorial covers three main scenarios: accessing individual nested fields in expressions, flattening nested arrays/objects for table display, and handling nested arrays like tags or addresses that need to be joined or rendered as comma-separated strings. Throughout, we emphasize safe access patterns using optional chaining (?.) to prevent errors when fields are absent.

## Before you start

- A Retool app with a REST API or SQL query returning nested JSON data
- Basic JavaScript knowledge including dot notation and array methods
- Familiarity with Retool transformers
- Understanding of Retool's {{ }} expression syntax

## Step-by-step guide

### 1. Inspect the Nested Data Shape in the State Panel

Before writing any expressions, understand your data's structure. Run your query, then open the State panel (left sidebar → State tab). Find your query (e.g. query1) and expand it to see the data structure. You can also open browser DevTools (F12) and look at the network response.

For a REST API, the response might look like: { users: [ { id, profile: { name, email, address: { city, state } }, tags: ['vip', 'premium'] } ] }. Map out the nesting depth before writing accessor expressions.

```
// Example nested API response shape:
// query1.data = {
//   users: [
//     {
//       id: 1,
//       profile: {
//         name: 'Alice Smith',
//         email: 'alice@example.com',
//         address: { city: 'Austin', state: 'TX' }
//       },
//       tags: ['vip', 'premium'],
//       stats: { order_count: 15, total_spent: 2450.00 }
//     }
//   ]
// }
```

**Expected result:** You have a clear picture of your API response's nesting structure.

### 2. Access Nested Fields with Dot Notation in Expressions

Use dot notation inside {{ }} expressions to drill into nested objects. Chain properties as deep as needed. If the API wraps results in an envelope like { data: [...] }, start with query1.data.data or query1.data.users.

For displaying a single selected user's nested city in a text component, bind the text component's value to the nested path.

```
// REST API with envelope: query1.data = { users: [...] }
// Access the array:
{{ query1.data.users }}

// Access the first user's email:
{{ query1.data.users[0].profile.email }}

// Access selected row's nested city (in master-detail layout):
{{ table1.selectedRow.profile?.address?.city ?? 'City not set' }}

// Access a nested stats field:
{{ table1.selectedRow.stats?.total_spent?.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) }}
```

**Expected result:** Text components and other bindings correctly display nested field values from the query response.

### 3. Unwrap API Envelopes in the Query Transformer

If your REST API wraps its data in an envelope (e.g. { data: [...], total: 100, page: 1 }), the query-attached transformer is the best place to unwrap it. This way, query1.data directly exposes the array and every table or component can bind to it simply with {{ query1.data }}.

```
// Query transformer (in the Transform tab of your REST query):
// The API returns: { data: [...users...], total: 450, page: 1 }

// Extract just the users array:
return data.data || data.users || data.results || data;

// Or if you need to keep the metadata:
return {
  users: data.data || [],
  total: data.total || 0,
  currentPage: data.page || 1
};
// Then access as query1.data.users in the table
```

**Expected result:** query1.data directly returns the array (or structured object) without needing deep access paths in every binding.

### 4. Flatten Nested Objects for Table Display

Retool tables display flat key-value objects best. When your data has nested objects (like profile.name, profile.email), flatten them in a transformer so each row is a flat object. Use the spread operator or explicit field mapping to promote nested fields to the top level.

```
// Transformer: flattenedUsers
// Input: query1.data (array with nested profile objects)
// Output: flat objects ready for table display

const users = query1.data || [];

return users.map(user => ({
  id: user.id,
  name: user.profile?.name ?? 'Unknown',
  email: user.profile?.email ?? '',
  city: user.profile?.address?.city ?? '',
  state: user.profile?.address?.state ?? '',
  tags: Array.isArray(user.tags) ? user.tags.join(', ') : '',
  orderCount: user.stats?.order_count ?? 0,
  totalSpent: user.stats?.total_spent ?? 0
}));
```

**Expected result:** The table displays a flat, readable structure with all nested fields promoted to top-level columns.

### 5. Display Nested Arrays as Tags or Lists

When a row has an array field (like tags, categories, or permissions), you have several display options. The simplest is joining the array into a comma-separated string in a transformer. For richer display, use Retool's Tag column type or an HTML template in a Text column.

In a calculated column using {{ currentRow }}: {{ Array.isArray(currentRow.tags) ? currentRow.tags.join(', ') : currentRow.tags }}

```
// In a Table column 'Value' expression:
{{ Array.isArray(currentRow.tags) ? currentRow.tags.join(', ') : (currentRow.tags ?? 'None') }}

// For a count of items in an array field:
{{ (currentRow.permissions || []).length + ' permissions' }}

// In a transformer, convert nested array of objects to string:
return data.map(row => ({
  ...row,
  // Convert [{name: 'admin'}, {name: 'editor'}] to 'admin, editor'
  roleNames: Array.isArray(row.roles)
    ? row.roles.map(r => r.name).join(', ')
    : ''
}));
```

**Expected result:** Nested arrays are displayed as readable strings or tag lists in the table.

### 6. Handle Deeply Nested Objects with Optional Chaining

For deeply nested structures (3+ levels), optional chaining prevents cascading null errors. Use ?. between each accessor and ?? to provide fallback values. This pattern is essential when working with real-world API responses where any field might be missing.

Retool's {{ }} expressions support modern JavaScript including optional chaining and nullish coalescing.

```
// Safe access to deep nesting:
{{ query1.data[0]?.profile?.address?.city ?? 'Unknown' }}

// In a transformer:
return data.map(row => ({
  id: row.id,
  city: row?.profile?.address?.city ?? 'Unknown',
  primaryPhone: row?.contact?.phones?.[0]?.number ?? 'N/A',
  latestOrderDate: row?.orders?.[0]?.created_at
    ? new Date(row.orders[0].created_at).toLocaleDateString()
    : 'No orders',
  metadataValue: row?.metadata?.customFields?.preferredContact ?? 'email'
}));
```

**Expected result:** No 'cannot read property of null/undefined' errors even when API fields are partially missing.

### 7. Use the JSON Explorer Component for Interactive Inspection

Retool includes a JSON Explorer component that lets users interactively expand and browse nested JSON. This is useful for admin tools, API debuggers, or any situation where users need to see the raw structure of a response.

Drag a JSON Explorer component onto your canvas. In the Inspector, set its Data property to {{ query1.data }} or {{ table1.selectedRow }}. Users can expand nodes to drill into nested structures.

**Expected result:** A JSON Explorer component renders on the page allowing interactive drill-down into the nested data structure.

### 8. Handle Nested Data in Form Pre-filling

When pre-filling a form from a selected table row that has nested data, you need to access the nested paths explicitly. For example, pre-filling a city text input from a selected user's nested address requires {{ table1.selectedRow.profile?.address?.city }} as the default value.

Alternatively, flatten the selected row data into a temporary state variable using a JS Query triggered on row selection.

```
// Form field default values from nested selected row:
// Name input default: {{ table1.selectedRow.profile?.name ?? '' }}
// City input default: {{ table1.selectedRow.profile?.address?.city ?? '' }}
// State input default: {{ table1.selectedRow.profile?.address?.state ?? '' }}

// JS Query triggered on row select to flatten into a state var:
// Event handler: table1 → Row selected → Run JS Query 'flattenSelectedRow'
const row = table1.selectedRow;
await selectedUserState.setValue({
  name: row.profile?.name ?? '',
  email: row.profile?.email ?? '',
  city: row.profile?.address?.city ?? '',
  state: row.profile?.address?.state ?? '',
  tags: row.tags?.join(', ') ?? ''
});
// Note: setValue() is async — do not read selectedUserState immediately after
```

**Expected result:** Form fields are correctly pre-filled with values from the selected table row's nested structure.

## Complete code example

File: `Transformer: flattenApiResponse`

```javascript
// Transformer: flattenApiResponse
// Input: query1.data from a REST API with nested structure
// API response shape:
// {
//   users: [{
//     id, profile: { name, email, address: { city, state } },
//     tags: [...strings],
//     stats: { order_count, total_spent },
//     roles: [{ id, name }]
//   }]
// }
//
// Access as: {{ flattenApiResponse.value }}

const rawData = query1.data;

// Handle both envelope and direct array responses
const users = Array.isArray(rawData)
  ? rawData
  : (rawData?.users || rawData?.data || []);

return users.map(user => ({
  // Core fields
  id: user.id,
  name: user?.profile?.name ?? 'Unknown',
  email: user?.profile?.email ?? '',

  // Address (nested 3 levels deep)
  city: user?.profile?.address?.city ?? '',
  state: user?.profile?.address?.state ?? '',
  fullAddress: [
    user?.profile?.address?.city,
    user?.profile?.address?.state
  ].filter(Boolean).join(', ') || 'No address',

  // Arrays as strings
  tags: Array.isArray(user.tags) ? user.tags.join(', ') : '',
  roles: Array.isArray(user.roles)
    ? user.roles.map(r => r.name).join(', ')
    : '',

  // Stats
  orderCount: user?.stats?.order_count ?? 0,
  totalSpent: user?.stats?.total_spent ?? 0,
  totalSpentFormatted: (user?.stats?.total_spent ?? 0).toLocaleString(
    'en-US', { style: 'currency', currency: 'USD' }
  )
}));
```

## Common mistakes

- **Using query1.data.field when the API returns an envelope like { data: [...] }** — undefined Fix: Access the inner array with query1.data.data or query1.data.users. Or unwrap the envelope in the query transformer so query1.data directly returns the array.
- **Accessing deep nesting without optional chaining causing 'Cannot read property of null'** — undefined Fix: Add ?. between each nested level: query1.data[0]?.profile?.address?.city. Without it, a null profile crashes the entire expression.
- **Displaying an array field in a table column without converting it to a string** — undefined Fix: Use .join(', ') or .join(' | ') to convert arrays to strings for display. Or use a calculated column expression: {{ Array.isArray(currentRow.tags) ? currentRow.tags.join(', ') : '' }}
- **setValue() called after reading nested state without awaiting** — undefined Fix: setValue() is async. After calling await myState.setValue(nestedData), you can safely read the flattened values in the next line. Without await, the read happens before the value is set.

## Best practices

- Always null-check nested access with optional chaining (?.) — real-world API responses frequently have missing fields that will crash your transformer without guards.
- Flatten nested data in a transformer before binding to a table — tables work best with flat objects, and deeply nested access in column expressions is harder to maintain.
- Use the JSON Explorer component during development to quickly navigate and understand an unfamiliar API response structure.
- Provide ?? fallback values for every nested access so users see 'Unknown' or 'N/A' rather than blank cells.
- Convert array fields to strings (with .join()) before table display unless using a custom column type that can handle arrays.
- Use the query's rawData property for debugging — it always contains the original unmodified response.
- For REST APIs, handle envelope patterns by detecting whether the response is an array or object with a data/users/results key.

## Frequently asked questions

### How do I access nested JSON in a Retool {{ }} expression?

Use dot notation: {{ query1.data[0].profile.email }}. For safety with potentially-null intermediate values, use optional chaining: {{ query1.data[0]?.profile?.email ?? 'Not provided' }}. If the API wraps data in an envelope, access the inner array first: {{ query1.data.users[0].profile.email }}.

### Why does my table show [object Object] instead of nested field values?

This happens when a table column is bound to a nested object rather than a string value. Add a calculated column using {{ JSON.stringify(currentRow.nestedField) }} for debugging, or better yet, flatten the nested object in a transformer before binding to the table so each column maps to a scalar value.

### Can I use optional chaining (?.) in Retool {{ }} expressions?

Yes. Retool's expression engine supports modern JavaScript including optional chaining (?.) and nullish coalescing (??). You can write {{ table1.selectedRow?.profile?.address?.city ?? 'Unknown' }} directly in any property field that accepts expressions.

### How do I display an array of objects from a nested field in a Retool table column?

Convert the array to a string in the column's Value expression: {{ currentRow.roles?.map(r => r.name).join(', ') ?? '' }}. For a count, use {{ currentRow.permissions?.length ?? 0 }}. Alternatively, pre-process the arrays in a transformer and store them as pre-formatted strings in the flat object.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-handle-nested-data-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-handle-nested-data-in-retool
