R Academy · Lesson

Handling Nested JSON Structures

Flatten deeply nested JSON into tidy data frames for analysis.

Lesson 4 of 413 steps

Handling Nested JSON Structures is a free R Academy lesson on CoddyKit. This is lesson 4 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, and your progress syncs across the web and the CoddyKit app. The R Academy course includes 4 lessons in total.

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 nesting
  • fromJSON(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 frames
  • jsonlite::flatten(df) — flatten nested data-frame columns post-parse
  • Always guard against NULL with if (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)
Free to start

Learn R with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
43
Lessons
159

Frequently Asked Questions

Is the “Handling Nested JSON Structures” lesson free?

Yes — any 3 lessons of the R Academy learning path, including “Handling Nested JSON Structures”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The R Academy course includes 4 lessons in total.

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, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.

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

  1. Parsing JSON with jsonlite
  2. Making HTTP Requests with httr2
  3. Consuming REST APIs in R
  4. Handling Nested JSON Structures
← Back to R Academy