Handling Nested JSON Structures
Flatten deeply nested JSON into tidy data frames for analysis.
Handling Nested JSON Structures is a free R Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Nested JSON Is Tricky
REST APIs often return deeply nested JSON where a single field may contain an array of objects, which in turn contain more objects. Flattening this structure into a tidy data frame requires understanding how jsonlite, purrr, and tidyr work together.
# Example nested JSON from a REST API:
json_str <- '{
"user": {
"id": 1,
"name": "Alice",
"orders": [
{"order_id": 101, "total": 59.99, "status": "shipped"},
{"order_id": 102, "total": 24.50, "status": "pending"}
]
}
}'
# The challenge: 'orders' is an array of objects inside 'user'
cat('Nested JSON loaded as string, length:', nchar(json_str))fromJSON() — Basic Parsing
jsonlite::fromJSON() converts a JSON string or file path into R objects. Simple flat JSON becomes a list or data frame. Nested JSON becomes a nested list — arrays of objects become data frames stored inside list columns.
library(jsonlite)
# Parse flat JSON
flat_json <- '{"name": "Alice", "age": 30, "score": 95.5}'
result <- fromJSON(flat_json)
cat('Name:', result$name, '\n')
cat('Age: ', result$age, '\n')
# Parse an array of objects — becomes a data frame
array_json <- '[{"id":1,"val":10},{"id":2,"val":20},{"id":3,"val":30}]'
df <- fromJSON(array_json)
cat('Class:', class(df), '\n')
print(df)fromJSON() with flatten = TRUE
The flatten = TRUE argument tells fromJSON() to recursively unpack nested data frames into columns with dot-separated names. This works well for one level of nesting and is the quickest way to handle moderately nested JSON.
library(jsonlite)
json_str <- '[{
"id": 1,
"name": "Alice",
"address": {"city": "Berlin", "country": "Germany"}
},{
"id": 2,
"name": "Bob",
"address": {"city": "Paris", "country": "France"}
}]'
# Without flatten:
nested_df <- fromJSON(json_str, flatten = FALSE)
cat('address class:', class(nested_df$address), '\n')
# With flatten = TRUE:
flat_df <- fromJSON(json_str, flatten = TRUE)
cat('Columns:', names(flat_df), '\n')
print(flat_df)Nested Arrays Become List-Columns
When a JSON field contains an array of objects, fromJSON() stores it as a list-column in the data frame — each cell holds a data frame. You must explicitly access or unnest these.
library(jsonlite)
json_str <- '[{
"user_id": 1,
"tags": ["R", "Python", "SQL"]
},{
"user_id": 2,
"tags": ["Java", "Kotlin"]
}]'
df <- fromJSON(json_str)
cat('tags column class:', class(df$tags), '\n')
# Access tags for user 1:
cat('User 1 tags:', df$tags[[1]], '\n')
cat('User 2 tags:', df$tags[[2]])purrr::map() — Extract Nested Fields
purrr::map() applies a function to each element of a list. When each element is a named list (parsed JSON object), you can pass a string to extract a named field from every element — a concise replacement for a loop.
library(jsonlite)
library(purrr)
json_str <- '[{
"id": 1,
"meta": {"score": 88, "grade": "B"}
},{
"id": 2,
"meta": {"score": 95, "grade": "A"}
},{
"id": 3,
"meta": {"score": 72, "grade": "C"}
}]'
records <- fromJSON(json_str, simplifyDataFrame = FALSE)
# Extract 'score' from each record's 'meta' object
scores <- map_dbl(records, function(r) r$meta$score)
grades <- map_chr(records, function(r) r$meta$grade)
cat('Scores:', scores, '\n')
cat('Grades:', grades)purrr::map() with String Shortcut
purrr::map(list, 'field_name') is shorthand for extracting a named field from every element — equivalent to map(list, function(x) x[['field_name']]). Use map_chr(), map_dbl(), etc. to get typed atomic vectors instead of lists.
library(jsonlite)
library(purrr)
json_str <- '[{"name":"Alice","score":90},{"name":"Bob","score":78},{"name":"Carol","score":85}]'
# Parse as list of lists
records <- fromJSON(json_str, simplifyDataFrame = FALSE)
# String shortcut to extract field
names_vec <- map_chr(records, 'name')
scores_vec <- map_dbl(records, 'score')
cat('Names: ', names_vec, '\n')
cat('Scores:', scores_vec, '\n')
# Build a clean data frame
clean_df <- data.frame(name = names_vec, score = scores_vec)
print(clean_df)Deep Nesting with map() Chains
For deeply nested JSON you chain multiple map() calls. Each call descends one level. Use map(list, 'field') at each level and apply a typed map_*() at the innermost step to extract the final value.
library(jsonlite)
library(purrr)
json_str <- '[{
"id": 1,
"company": {"hq": {"city": "Berlin", "country": "Germany"}}
},{
"id": 2,
"company": {"hq": {"city": "Tokyo", "country": "Japan"}}
}]'
records <- fromJSON(json_str, simplifyDataFrame = FALSE)
# Navigate: records -> company -> hq -> city
cities <- map_chr(records, function(r) r$company$hq$city)
cat('Cities:', cities, '\n')
# Or using nested map shortcut:
ids <- map_int(records, 'id')
cat('IDs:', ids)tidyr::unnest() — Flatten List-Columns
tidyr::unnest() expands a list-column that contains data frames, creating one row per nested element. This is the standard tidy approach to flattening one-to-many relationships in JSON data.
library(jsonlite)
library(tidyr)
library(dplyr)
json_str <- '[{
"user_id": 1,
"orders": [{"oid":101,"total":50},{"oid":102,"total":30}]
},{
"user_id": 2,
"orders": [{"oid":103,"total":80}]
}]'
df <- fromJSON(json_str)
cat('Before unnest, rows:', nrow(df), '\n')
cat('orders class:', class(df$orders), '\n')
# Unnest expands one row per order
expanded <- unnest(df, cols = orders)
cat('After unnest, rows:', nrow(expanded), '\n')
print(expanded)jsonlite::flatten() on Data Frames
jsonlite::flatten() works on an already-parsed data frame (not a JSON string), recursively expanding any nested data-frame columns into dot-separated name columns. It is useful after fromJSON(..., flatten = FALSE) when you want to flatten as a post-processing step.
library(jsonlite)
json_str <- '[{
"id": 1,
"profile": {"age": 25, "city": "Rome"}
},{
"id": 2,
"profile": {"age": 31, "city": "Oslo"}
}]'
# Parse without auto-flatten
nested <- fromJSON(json_str, flatten = FALSE)
cat('Columns before flatten:', names(nested), '\n')
cat('profile class:', class(nested$profile), '\n')
# Apply flatten() post-hoc
flat <- flatten(nested)
cat('Columns after flatten:', names(flat), '\n')
print(flat)Dealing with Nulls in Nested JSON
JSON null values parse to NULL in R, which causes problems when building data frames — a NULL in a list drops the element entirely. Use purrr::map() with a .default argument or %||% to replace missing values safely.
library(jsonlite)
library(purrr)
json_str <- '[{"id":1,"email":"alice@example.com"},{"id":2,"email":null},{"id":3,"email":"carol@example.com"}]'
records <- fromJSON(json_str, simplifyDataFrame = FALSE)
# Unsafe: NULL drops the element
# emails <- map_chr(records, 'email') # ERROR on null
# Safe: provide a default for missing values
emails <- map_chr(records, function(r) {
if (is.null(r$email)) NA_character_ else r$email
})
cat('Emails:', emails)
cat('NAs:', sum(is.na(emails)))Complete Pipeline: API JSON to Tidy Data Frame
Putting it all together: a realistic pipeline that parses nested JSON from an API, extracts fields with purrr, handles nulls, and produces a tidy data frame ready for analysis.
library(jsonlite)
library(purrr)
library(dplyr)
# Simulated API response
api_json <- '[{
"id": 1, "name": "Alice",
"stats": {"score": 92, "rank": 1}
},{
"id": 2, "name": "Bob",
"stats": null
},{
"id": 3, "name": "Carol",
"stats": {"score": 85, "rank": 3}
}]'
records <- fromJSON(api_json, simplifyDataFrame = FALSE)
result <- tibble(
id = map_int(records, 'id'),
name = map_chr(records, 'name'),
score = map_dbl(records, function(r) if (is.null(r$stats)) NA_real_ else r$stats$score),
rank = map_int(records, function(r) if (is.null(r$stats)) NA_integer_ else r$stats$rank)
)
print(result)Quick Check
You have a data frame df where the column orders is a list-column containing data frames (one per user). Which function expands this into one row per order?
Nested JSON — Key Takeaways
Handling nested JSON in R requires a layered toolkit:
fromJSON(json, flatten = TRUE)— auto-flatten one level of nestingfromJSON(json, simplifyDataFrame = FALSE)— get a list of lists for manual processing- Nested arrays → list-columns in the resulting data frame
purrr::map_chr/dbl/int(list, 'field')— extract typed values from every element- Chained
map()calls descend through deep nesting levels tidyr::unnest(df, cols = col)— expand list-columns of data framesjsonlite::flatten(df)— flatten nested data-frame columns post-parse- Always guard against
NULLwithif (is.null(x)) NA else x
library(jsonlite)
library(purrr)
# Quick reference:
json <- '[{"id":1,"info":{"val":10}},{"id":2,"info":null}]'
recs <- fromJSON(json, simplifyDataFrame = FALSE)
# Safe extraction with null guard
vals <- map_dbl(recs, function(r) {
if (is.null(r$info)) NA_real_ else r$info$val
})
result <- data.frame(id = map_int(recs, 'id'), val = vals)
print(result)Frequently asked questions
Is the “Handling Nested JSON Structures” lesson free?
Yes — the full text of “Handling Nested JSON Structures” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.
What will I learn in “Handling Nested JSON Structures”?
Flatten deeply nested JSON into tidy data frames for analysis. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start R Academy?
No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Handling Nested JSON Structures” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this R Academy lesson?
Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Parsing JSON with jsonlite
- Making HTTP Requests with httr2
- Consuming REST APIs in R
- Handling Nested JSON Structures