Skip to main content
RapidDev - Software Development Agency
retool-tutorial

How to Handle Nested Data in Retool

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.

What you'll learn

  • How to access nested JSON fields using dot notation in {{ }} expressions
  • How to flatten nested API responses in a transformer before binding to a table
  • How to use the JSON Explorer component to visualize deep structures
  • How to display nested arrays (like tags or addresses) in table columns
  • How to safely access deeply nested fields with optional chaining
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read20-25 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
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.

Quick facts about this guide
FactValue
ToolRetool
DifficultyIntermediate
Time required20-25 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

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.

Prerequisites

  • 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.

typescript
1// Example nested API response shape:
2// query1.data = {
3// users: [
4// {
5// id: 1,
6// profile: {
7// name: 'Alice Smith',
8// email: 'alice@example.com',
9// address: { city: 'Austin', state: 'TX' }
10// },
11// tags: ['vip', 'premium'],
12// stats: { order_count: 15, total_spent: 2450.00 }
13// }
14// ]
15// }

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.

typescript
1// REST API with envelope: query1.data = { users: [...] }
2// Access the array:
3{{ query1.data.users }}
4
5// Access the first user's email:
6{{ query1.data.users[0].profile.email }}
7
8// Access selected row's nested city (in master-detail layout):
9{{ table1.selectedRow.profile?.address?.city ?? 'City not set' }}
10
11// Access a nested stats field:
12{{ 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 }}.

typescript
1// Query transformer (in the Transform tab of your REST query):
2// The API returns: { data: [...users...], total: 450, page: 1 }
3
4// Extract just the users array:
5return data.data || data.users || data.results || data;
6
7// Or if you need to keep the metadata:
8return {
9 users: data.data || [],
10 total: data.total || 0,
11 currentPage: data.page || 1
12};
13// 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.

typescript
1// Transformer: flattenedUsers
2// Input: query1.data (array with nested profile objects)
3// Output: flat objects ready for table display
4
5const users = query1.data || [];
6
7return users.map(user => ({
8 id: user.id,
9 name: user.profile?.name ?? 'Unknown',
10 email: user.profile?.email ?? '',
11 city: user.profile?.address?.city ?? '',
12 state: user.profile?.address?.state ?? '',
13 tags: Array.isArray(user.tags) ? user.tags.join(', ') : '',
14 orderCount: user.stats?.order_count ?? 0,
15 totalSpent: user.stats?.total_spent ?? 0
16}));

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 }}

typescript
1// In a Table column 'Value' expression:
2{{ Array.isArray(currentRow.tags) ? currentRow.tags.join(', ') : (currentRow.tags ?? 'None') }}
3
4// For a count of items in an array field:
5{{ (currentRow.permissions || []).length + ' permissions' }}
6
7// In a transformer, convert nested array of objects to string:
8return data.map(row => ({
9 ...row,
10 // Convert [{name: 'admin'}, {name: 'editor'}] to 'admin, editor'
11 roleNames: Array.isArray(row.roles)
12 ? row.roles.map(r => r.name).join(', ')
13 : ''
14}));

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.

typescript
1// Safe access to deep nesting:
2{{ query1.data[0]?.profile?.address?.city ?? 'Unknown' }}
3
4// In a transformer:
5return data.map(row => ({
6 id: row.id,
7 city: row?.profile?.address?.city ?? 'Unknown',
8 primaryPhone: row?.contact?.phones?.[0]?.number ?? 'N/A',
9 latestOrderDate: row?.orders?.[0]?.created_at
10 ? new Date(row.orders[0].created_at).toLocaleDateString()
11 : 'No orders',
12 metadataValue: row?.metadata?.customFields?.preferredContact ?? 'email'
13}));

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.

typescript
1// Form field default values from nested selected row:
2// Name input default: {{ table1.selectedRow.profile?.name ?? '' }}
3// City input default: {{ table1.selectedRow.profile?.address?.city ?? '' }}
4// State input default: {{ table1.selectedRow.profile?.address?.state ?? '' }}
5
6// JS Query triggered on row select to flatten into a state var:
7// Event handler: table1 → Row selected → Run JS Query 'flattenSelectedRow'
8const row = table1.selectedRow;
9await selectedUserState.setValue({
10 name: row.profile?.name ?? '',
11 email: row.profile?.email ?? '',
12 city: row.profile?.address?.city ?? '',
13 state: row.profile?.address?.state ?? '',
14 tags: row.tags?.join(', ') ?? ''
15});
16// 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 working example

Transformer: flattenApiResponse
1// Transformer: flattenApiResponse
2// Input: query1.data from a REST API with nested structure
3// API response shape:
4// {
5// users: [{
6// id, profile: { name, email, address: { city, state } },
7// tags: [...strings],
8// stats: { order_count, total_spent },
9// roles: [{ id, name }]
10// }]
11// }
12//
13// Access as: {{ flattenApiResponse.value }}
14
15const rawData = query1.data;
16
17// Handle both envelope and direct array responses
18const users = Array.isArray(rawData)
19 ? rawData
20 : (rawData?.users || rawData?.data || []);
21
22return users.map(user => ({
23 // Core fields
24 id: user.id,
25 name: user?.profile?.name ?? 'Unknown',
26 email: user?.profile?.email ?? '',
27
28 // Address (nested 3 levels deep)
29 city: user?.profile?.address?.city ?? '',
30 state: user?.profile?.address?.state ?? '',
31 fullAddress: [
32 user?.profile?.address?.city,
33 user?.profile?.address?.state
34 ].filter(Boolean).join(', ') || 'No address',
35
36 // Arrays as strings
37 tags: Array.isArray(user.tags) ? user.tags.join(', ') : '',
38 roles: Array.isArray(user.roles)
39 ? user.roles.map(r => r.name).join(', ')
40 : '',
41
42 // Stats
43 orderCount: user?.stats?.order_count ?? 0,
44 totalSpent: user?.stats?.total_spent ?? 0,
45 totalSpentFormatted: (user?.stats?.total_spent ?? 0).toLocaleString(
46 'en-US', { style: 'currency', currency: 'USD' }
47 )
48}));

Common mistakes when handling Nested Data in Retool

Why it's a problem: Using query1.data.field when the API returns an envelope like { data: [...] }

How to avoid: 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.

Why it's a problem: Accessing deep nesting without optional chaining causing 'Cannot read property of null'

How to avoid: Add ?. between each nested level: query1.data[0]?.profile?.address?.city. Without it, a null profile crashes the entire expression.

Why it's a problem: Displaying an array field in a table column without converting it to a string

How to avoid: Use .join(', ') or .join(' | ') to convert arrays to strings for display. Or use a calculated column expression: {{ Array.isArray(currentRow.tags) ? currentRow.tags.join(', ') : '' }}

Why it's a problem: setValue() called after reading nested state without awaiting

How to avoid: 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.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a Retool REST API query (query1) that returns this nested JSON structure: { users: [{ id, profile: { name, email, address: { city, state } }, tags: ['vip', 'premium'], stats: { order_count, total_spent }, roles: [{ id, name }] }] }. Write a Retool standalone transformer that flattens this structure into a table-ready array of flat objects. Include proper null/undefined guards using optional chaining, convert array fields to comma-separated strings, and handle the API envelope (users wrapper). The transformer should return the flat array.

Retool Prompt

Create a transformer named 'flattenApiResponse' that takes query1.data (a REST API response with envelope { users: [...] }) and returns a flat array of objects. Each object should include: id, name (from profile.name), email (from profile.email), city and state (from profile.address), tags as a comma-separated string, and orderCount from stats.order_count. Use optional chaining for all nested accesses.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.