JSON Flattening Algorithm: From Nested Objects to Dot-Notation Keys
JSON Flattening Algorithm: From Nested Objects to Dot-Notation Keys#
Dealing with deeply nested JSON data is a common pain point in backend development. APIs often return data nested four or five levels deep, requiring a chain of property access: response.data.user.profile.address.city. A JSON flattening tool converts nested structures into single-level objects, making data access more direct.
The Essence of Flattening#
Transforming { "a": { "b": { "c": 1 } } } into { "a.b.c": 1 } — that’s flattening. The core is recursively traversing the object tree and concatenating paths with a delimiter.
function flatten(obj: any, delimiter = '.', prefix = '', result: Record<string, any> = {}): Record<string, any> {
// Edge case: non-object or array, assign directly
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
result[prefix || 'root'] = obj
return result
}
// Iterate through object properties
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = prefix ? `${prefix}${delimiter}${key}` : key
const value = obj[key]
// Nested object: recurse
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
flatten(value, delimiter, newKey, result)
} else {
// Leaf node: assign directly
result[newKey] = value
}
}
}
return result
}
Key insight: The result object is passed as a parameter to avoid creating new objects on each recursion. prefix tracks the current path.
Unflattening: Rebuilding Nested Structure from Dot-Notation Keys#
The reverse operation is more complex: transforming "a.b.c": 1 back into { a: { b: { c: 1 } } }. This requires building objects layer by layer.
function unflatten(obj: Record<string, any>, delimiter = '.'): any {
const result: any = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const keys = key.split(delimiter) // "a.b.c" → ["a", "b", "c"]
let current = result
// Create nested objects layer by layer (except the last)
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]
if (!(k in current)) {
current[k] = {}
}
current = current[k]
}
// Assign to the leaf node
current[keys[keys.length - 1]] = obj[key]
}
}
return result
}
Here, a current pointer traverses deeper, creating empty objects for missing keys. The value is assigned at the leaf node.
Handling Edge Cases#
1. What About Arrays?#
The implementation above treats arrays as plain values. To preserve array structure:
if (Array.isArray(value)) {
// Option 1: Use index as key
value.forEach((item, index) => {
const arrayKey = `${newKey}[${index}]`
if (typeof item === 'object' && item !== null) {
flatten(item, delimiter, arrayKey, result)
} else {
result[arrayKey] = item
}
})
}
2. Keys Containing the Delimiter#
If the original key already contains ., it creates ambiguity. Solutions:
- Use an uncommon delimiter (like
/or|) - Escape the delimiter:
"a\.b"→ actual key isa.b - Use special format:
{"a.b": 1}flattens to{"['a.b']": 1}
const newKey = prefix ? `${prefix}${delimiter}${JSON.stringify(key)}` : key
// Output: {"a.b": 1} → {'"a"["b"]': 1} or wrap in quotes
3. Empty Objects and Null#
const test1 = { a: {} } // After flatten: {} (empty object produces no keys)
const test2 = { a: null } // After flatten: {"a": null}
const test3 = { a: [] } // After flatten: {"a": []}
Empty objects have no properties to iterate, so no keys are produced. null and [] are valid values and are preserved.
Practical Use Cases#
1. Form Data Processing#
Complex forms often use nested objects:
{
"user": {
"name": "John",
"contact": {
"email": "john@example.com",
"phone": "+1234567890"
}
}
}
After flattening, you can access directly with formData["user.contact.email"], making it easier for validation libraries like Yup to process.
2. Database Storage#
MongoDB supports nested documents, but relational databases require flattening. After flattening JSON, each key corresponds to a column:
CREATE TABLE user_data (
user_name VARCHAR(100),
user_contact_email VARCHAR(100),
user_contact_phone VARCHAR(20)
);
3. Configuration File Merging#
Multiple config files can be distinguished by prefix:
// config1.json
{ "database": { "host": "localhost" } }
// config2.json
{ "database": { "port": 3306 } }
Flatten, merge, then unflatten:
const merged = { ...flatten(config1), ...flatten(config2) }
const final = unflatten(merged)
// { database: { host: "localhost", port: 3306 } }
Performance Considerations#
Time Complexity#
Both flattening and unflattening are O(n), where n is the number of leaf nodes.
Depth Limit#
JavaScript recursion has a call stack limit (typically around 10,000 levels). For extremely deep nested objects, use iteration instead:
function flattenIterative(obj: any, delimiter = '.'): Record<string, any> {
const result: Record<string, any> = {}
const stack: Array<{ node: any, prefix: string }> = [{ node: obj, prefix: '' }]
while (stack.length > 0) {
const { node, prefix } = stack.pop()!
for (const key in node) {
if (node.hasOwnProperty(key)) {
const newKey = prefix ? `${prefix}${delimiter}${key}` : key
const value = node[key]
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
stack.push({ node: value, prefix: newKey })
} else {
result[newKey] = value
}
}
}
}
return result
}
Using a stack to simulate recursion avoids call stack overflow.
Online Tool#
Based on the implementation above, I built an online JSON flattening tool: JSON Flatten
Features:
- Bidirectional conversion (flatten / unflatten)
- Custom delimiter (default:
.) - Supports nested objects, arrays, and null
- Real-time error feedback
Flattening is a small feature, but handling all edge cases properly takes effort. Hope this helps.
Related tools: JSON Formatter | JSON Diff