tidyr unnest_wider() in R: Spread Named Lists Into Columns

The unnest_wider() function in tidyr spreads a list-column where each cell is a NAMED LIST into multiple new columns, one per name. It is essential for unnesting JSON-shaped data.

⚡ Quick Answer
df |> unnest_wider(json_col)             # spread named list to columns
df |> unnest_wider(json_col, names_sep = "_") # prefix new column names
df |> unnest_wider(json_col, names_repair = "unique")
df |> unnest_longer(col)                  # different: vectors to rows
df |> unnest(col)                         # different: tibbles to rows

Need explanation? Read on for examples and pitfalls.

📊 Is unnest_wider() the right tool?
STARTlist column of named lists -> columnsunnest_wider()list column of tibbles (one row each) -> columnsunnest_wider() works toolist column of vectors -> rowsunnest_longer()JSON parsed via jsonlite:fromJSON -> columns: unnest_wider()handle name conflictsnames_sep / names_repair

What unnest_wider() does in one sentence

unnest_wider(data, col, names_sep = NULL) spreads a list-column where each cell is a named list (or 1-row tibble) into multiple new columns, named after the list elements. Each name in the list becomes a new column.

Syntax

unnest_wider(data, col, names_sep = NULL, names_repair = "check_unique"). names_sep prefixes column names.

Run live
Run live, no install needed. Every R block on this page runs in your browser. Click Run, edit the code, re-run instantly. No setup.
RSpread a JSON-like list column
library(tidyr) library(dplyr) df <- tibble( user = c("a","b"), details = list( list(age = 30, city = "NYC"), list(age = 25, city = "LA") ) ) df |> unnest_wider(details) #> user age city #> 1 a 30 NYC #> 2 b 25 LA

  
Tip
Use unnest_wider after jsonlite::fromJSON() to extract JSON object fields into separate columns. Common ETL pattern for API responses.

Five common patterns

1. Standard wide unnest

REach list name -> column
df |> unnest_wider(details)

  

2. Prefix new column names

RAvoid name conflicts
df |> unnest_wider(details, names_sep = "_") #> Columns become details_age, details_city

  

3. JSON parsing workflow

RParse JSON then unnest
library(jsonlite) df <- tibble(json = c('{"a":1,"b":2}', '{"a":3,"b":4}')) df |> mutate(parsed = lapply(json, fromJSON)) |> unnest_wider(parsed) #> json a b #> 1 {"a":1,"b":2} 1 2 #> 2 {"a":3,"b":4} 3 4

  

4. From a list of one-row tibbles

REach cell is a 1-row tibble
df <- tibble(id = 1:2, info = list(tibble(a = 1, b = 2), tibble(a = 3, b = 4))) df |> unnest_wider(info)

  

5. Handle missing names

RWhen some elements have different names
df <- tibble(id = 1:2, obj = list(list(a = 1, b = 2), list(a = 3))) df |> unnest_wider(obj) #> id a b #> 1 1 1 2 #> 2 2 3 NA <-- b missing in second list

  
Key Insight
unnest_wider is the standard tool for "JSON object -> columns". Combined with purrr::map() and jsonlite::fromJSON, it turns nested API responses into tabular data.

unnest_wider() vs unnest_longer() vs unnest() vs hoist

Function Input shape Output
unnest_wider() Named lists New columns
unnest_longer() Vectors New rows
unnest() Tibbles New rows
hoist() Named lists, specific elements Specific new columns

When to use which:

  • unnest_wider for ALL fields of a named list.
  • hoist for SPECIFIC fields by name.
  • unnest_longer for vectors.
  • unnest for tibbles.

A practical workflow

The "API response to data frame" pattern uses unnest_wider extensively.

RInteractive R
library(jsonlite) api_responses |> mutate(parsed = lapply(json_str, fromJSON)) |> unnest_wider(parsed) |> unnest_wider(user_info, names_sep = "_user") # nested object

  

JSON with nested objects requires multiple unnest_wider calls.

Common pitfalls

Pitfall 1: name conflicts. If the list contains a name that already exists in the data frame (e.g., both have an "id"), unnest_wider errors. Use names_sep or names_repair to handle.

Pitfall 2: confusing with unnest. unnest expects each cell to be a tibble; output is rows. unnest_wider also works on 1-row tibbles but more naturally on named lists; output is columns.

Warning
unnest_wider() may produce many NA values if the list elements have different names. Each row gets a column for EVERY name seen across all list cells.

Try it yourself

Try it: Spread a list column of list(score, grade) into two columns. Save to ex_wide.

RYour turn: list to columns
df <- tibble( student = c("a","b"), results = list(list(score = 90, grade = "A"), list(score = 75, grade = "C")) ) ex_wide <- df |> # your code here ex_wide #> Expected: 2 rows with student, score, grade columns

  
Click to reveal solution
RSolution
ex_wide <- df |> unnest_wider(results) ex_wide #> student score grade #> 1 a 90 A #> 2 b 75 C

  

Explanation: Each list's named elements become columns. score and grade are extracted from each cell.

After mastering unnest_wider, look at:

  • unnest_longer(): vectors to rows
  • unnest(): tibbles to rows
  • hoist(): extract specific elements
  • pack(): opposite (combine columns into list)
  • jsonlite::fromJSON(): JSON to R objects

FAQ

What does unnest_wider do in tidyr?

unnest_wider(data, col) spreads a list column where each cell is a named list (or 1-row tibble) into multiple new columns, named after the list elements.

What is the difference between unnest_wider and unnest_longer?

unnest_wider creates new COLUMNS (one per name). unnest_longer creates new ROWS (one per element). Different output shapes.

How do I avoid column name conflicts with unnest_wider?

Pass names_sep = "_" to prefix new column names with the original list-column's name, or use names_repair = "unique".

Can I use unnest_wider on JSON?

Yes. After parsing JSON with jsonlite::fromJSON(), unnest_wider spreads each top-level field into a column.

What if my list elements have different names?

unnest_wider creates a column for EVERY name seen across all cells. Cells without that name get NA in the corresponding column.

Prefer to learn by doing? Every track on the learning roadmap opens with a free section of interactive lessons, where you write the R yourself and the page tells you when it is wrong.