To extract all keys from a JSON object, you have to walk it recursively: a top-level key list misses everything nested inside objects and arrays, which is usually the part you need. This guide covers six ways to do it - a no-code online tool, recursive functions in JavaScript, Python, and Ruby, and the command-line tool jq - and then puts all of them through the same awkward document. That test found a bug in this guide's own JavaScript, a jq command that silently returns nothing, and one edge case that fools every method but one.
Why You Need to Extract JSON Keys
Before writing code to process a JSON response, you need a map of what fields are available. Key extraction tells you the full structure of a JSON document, including deeply nested fields that you might miss by reading the raw JSON. It is especially valuable when:
- Integrating a new third-party API for the first time and wanting to understand its full response shape
- Building field mappings for data transformations, ETL pipelines, or database imports
- Documenting a JSON schema for your team so everyone knows what fields exist
- Debugging why a parser cannot find a field - extracting keys confirms whether the field actually exists in the response
- Comparing two API responses to find what changed between versions
- Generating code (e.g. TypeScript interfaces or Python dataclasses) based on a JSON structure
Method 1: JSON Keyper - Instant, No Code
For quick one-off exploration, the fastest way to extract all JSON keys is to paste the document into JSON Keyper and click Extract Keys - no code, no terminal, no dependencies:
- Copy your JSON from your API client (Postman, Insomnia, browser DevTools, etc.)
- Paste it into the left input box on JSON Keyper
- Click Extract Keys
- Every key path appears in the right box -
user.address.city,items[0].name, etc. - Click Copy to copy all paths to your clipboard
This handles nested objects and arrays automatically, outputting paths in dot notation with bracket notation for array indices. If the JSON lives behind an API rather than on your clipboard, you can also run a curl command online and extract the keys from the live response in the same page.
Two options are worth knowing about, because they turn the raw path dump into something more directly usable. Collapse array indices merges items[0].sku and items[1].sku into a single items[].sku, which is what you want when documenting a schema rather than inspecting one record. And the format selector switches the same extracted structure into value-annotated paths, JSONPath expressions, an indented tree, or a generated TypeScript interface - saving you from writing the code in the sections below at all, for the common cases. Each also has its own page with worked examples: flatten nested JSON to dot-notation paths, generate JSONPath expressions, or generate TypeScript from JSON.
Method 2: JavaScript - Object.keys()
Object.keys() returns only the top-level keys of an object. It is useful for flat JSON but misses nested fields:
const data = {
"name": "Alice",
"age": 30,
"address": { "city": "London", "zip": "EC1A 1BB" },
"tags": ["developer", "admin"]
};
console.log(Object.keys(data));
// ["name", "age", "address", "tags"]
// NOTE: does NOT include "address.city" or "address.zip"
Method 3: Recursive JavaScript
To extract every nested key path, including keys inside arrays of objects, you need a recursive function:
function getAllKeys(value, prefix = '') {
const keys = [];
if (Array.isArray(value)) {
value.forEach((item, index) => {
keys.push(...getAllKeys(item, `${prefix}[${index}]`));
});
} else if (value !== null && typeof value === 'object') {
for (const key of Object.keys(value)) {
const path = prefix ? `${prefix}.${key}` : key;
keys.push(path);
keys.push(...getAllKeys(value[key], path));
}
}
return keys;
}
const keys = getAllKeys(JSON.parse(jsonString));
console.log(keys.join('\n'));
For the example above, this outputs:
name
age
address
address.city
address.zip
tags
Arrays are walked rather than listed, so tags appears once instead of as tags[0] and tags[1], while keys inside arrays of objects come out as items[0].sku. On the test document below it produces exactly the same 13 paths as JSON Keyper, in the same order.
Correction, October 1, 2026: the function previously shown here used for...in and handled arrays only one level deep, so an array of arrays came out as matrix[0].0 instead of matrix[0][0]. The version above replaces it.
Method 4: Python
Python's approach is clean and readable using a recursive function with generator-style logic:
import json
def get_all_keys(obj, prefix=''):
keys = []
if isinstance(obj, dict):
for k, v in obj.items():
full_key = f"{prefix}.{k}" if prefix else k
keys.append(full_key)
keys.extend(get_all_keys(v, full_key))
elif isinstance(obj, list):
for i, v in enumerate(obj):
indexed_key = f"{prefix}[{i}]"
keys.append(indexed_key)
keys.extend(get_all_keys(v, indexed_key))
return keys
# From a string
data = json.loads(json_string)
print('\n'.join(get_all_keys(data)))
# From a file
with open('response.json') as f:
data = json.load(f)
print('\n'.join(get_all_keys(data)))
Method 5: Ruby
require 'json'
def get_all_keys(obj, prefix = '')
keys = []
case obj
when Hash
obj.each do |k, v|
full_key = prefix.empty? ? k.to_s : "#{prefix}.#{k}"
keys << full_key
keys.concat(get_all_keys(v, full_key))
end
when Array
obj.each_with_index do |v, i|
indexed_key = "#{prefix}[#{i}]"
keys << indexed_key
keys.concat(get_all_keys(v, indexed_key))
end
end
keys
end
data = JSON.parse(File.read('response.json'))
puts get_all_keys(data).join("\n")
Method 6: jq - Command Line
jq is a command-line JSON processor available on Linux, macOS, and Windows. These commands were run with jq 1.7:
# Top-level keys, sorted alphabetically (keys_unsorted keeps document order)
jq 'keys' data.json
# Every key path, in the same style as the functions above
jq -r 'paths | select(last | type == "string")
| map(if type == "number" then "[\(.)]" else "." + . end)
| join("") | ltrimstr(".")' data.json
# Every distinct key name, at any depth
jq -c '[paths | last | strings] | unique' data.json
# One nested value by path
jq '.user.address.city' data.json
Two commands that circulate widely do not do what they appear to. jq '[paths | select(type == "string")]' - often given as "all string keys" - always returns [], because paths produces arrays, never strings. And jq 'paths(scalars)' skips every key whose value is null or false, along with empty objects and arrays: it is built on select, which treats both of those values as false, so the paths are dropped without a warning.
jq is the right choice for scripts and CI pipelines. It is not, however, a way around memory limits on large files: plain jq parses the whole document before your filter runs. The large JSON files guide measures this - about nine times the file size in memory - and shows how --stream avoids it.
Tested: what each method does with awkward JSON
Every method above handles a tidy nested object. To see where they differ, I ran each of them, exactly as printed, on one small document built from the cases that cause trouble in real data:
{"user.name": "a key with a dot", "user": {"name": "Ada", "tags": ["a", "b"], "address": null},
"items": [{"sku": "A"}, {"sku": "B", "qty": 2}], "empty": {}, "list": [],
"matrix": [[1, 2], [3]], "0": "numeric-looking key"}
| Method | Paths | Array of arrays | null and empty values | Key order |
|---|---|---|---|---|
| JSON Keyper | 13 | matrix only | Kept | "0" moved first |
| Recursive JavaScript (above) | 13 | matrix only | Kept | "0" moved first |
| Recursive JavaScript (previous version) | 18 | matrix[0].0 - wrong | Kept | "0" moved first |
| Python, Ruby | 22 | matrix[0][0] | Kept | Document order |
jq paths command (above) | 13 | matrix only | Kept | Document order |
jq paths(scalars) | 11 | matrix.0.0 | Dropped | Document order |
Three things stand out:
- Every method prints
user.nametwice - once for the top-level key literally called"user.name", once fornameinsideuser- and no dot-notation output can tell them apart. Only JSONPath can: JSON Keyper's JSONPath output writes them as$['user.name']and$.user.name. If your data's keys might contain dots, use that format. - JavaScript reorders keys. Any key that looks like a non-negative integer -
"0"here - is listed before all the others, because that is how JavaScript orders object properties. Python, Ruby, and jq keep document order. This rarely matters for a key list, but it explains output that looks shuffled. - Python and Ruby list every array position -
user.tags[0],user.tags[1],matrix[1][0]- which is why they produce 22 paths. That is complete, but noisy for a document with long arrays; collapsing indices, as JSON Keyper's Collapse array indices option does, gives one line per distinct field instead.
Handling edge cases
- Keys with dots - dot-notation paths are ambiguous; use JSONPath, which quotes such keys.
nullvalues - the key exists and should be listed. Watch for tools built onpaths(scalars), which drop it - and drop keys whose value isfalse, too.- Empty objects and arrays -
{}and[]contain no keys, but the key that holds them still belongs in the list; otherwise a field that is merely empty in your sample disappears from your field mapping. - Numeric-looking keys -
"0"is a valid object key, not an array index. JavaScript lists such keys first. - Very deep documents - recursive functions use the call stack, so pathological nesting can overflow it; an explicit stack avoids that.
Which Method Should You Use?
| Scenario | Best Method |
|---|---|
| Quick one-off exploration | JSON Keyper |
| JSON from a live API | Run the curl command online, then extract |
| Keys that may contain dots | JSONPath output |
| Shell scripts or CI pipelines | jq |
| Python data pipeline | Python recursive function |
| Node.js / browser application | JavaScript recursive function |
| Top-level keys only, flat JSON | Object.keys() |
| Ruby application | Ruby recursive function |
Extract All JSON Keys - No Code Required
Paste any JSON and get every nested key path in one click, with types, JSONPath, or a TypeScript type. Free, in your browser, no login.
Open the JSON Key Extractor