How to Unflatten a JavaScript Object with Dot Notation into Nested Objects and Arrays
In JavaScript, working with nested data structures is common, but there are scenarios where data is provided in a "flattened" format—often using dot notation (e.g., user.name) or bracket notation for arrays (e.g., hobbies[0]). Flattened objects are frequently encountered in API responses, form data serialization, or configuration files, where nested structures are flattened for simplicity.
Unflattening is the process of converting such flattened objects back into their original nested form, with objects and arrays intact. For example, converting { 'user.name': 'John', 'user.hobbies[0]': 'reading' } into { user: { name: 'John', hobbies: ['reading'] } }.
This blog will guide you through understanding flattened objects, the challenges of unflattening, and a step-by-step implementation to unflatten objects with dot notation and array brackets.
Table of Contents#
- Understanding Flattened and Unflattened Objects
- Key Challenges in Unflattening
- Step-by-Step Guide to Unflattening
- Handling Arrays in Dot Notation
- Edge Cases and Solutions
- Practical Examples
- Testing the Unflatten Function
- Conclusion
- References
Understanding Flattened and Unflattened Objects#
Flattened Objects#
A flattened object represents nested structures using concatenated keys with dot notation (for objects) and brackets (for arrays). For example:
const flattened = {
'name': 'Alice',
'details.age': 25,
'details.hobbies[0]': 'reading',
'details.hobbies[1]': 'gaming',
'details.address.city': 'New York',
'scores[0]': 90,
'scores[1]': 85
};Unflattened Objects#
The unflattened version of the above object restores the nested structure:
const unflattened = {
name: 'Alice',
details: {
age: 25,
hobbies: ['reading', 'gaming'],
address: {
city: 'New York'
}
},
scores: [90, 85]
};The goal is to write a function that converts flattened into unflattened.
Key Challenges in Unflattening#
Unflattening isn’t trivial due to several edge cases:
- Arrays vs. Objects: Distinguishing between array indices (e.g.,
hobbies[0]) and object keys (e.g.,address.city). - Nested Structures: Building deeply nested objects/arrays without overwriting existing values.
- Array Indices: Handling non-sequential indices (e.g.,
hobbies[2]whenhobbies[0]is missing) and ensuring arrays are properly initialized. - Conflicting Keys: Cases where a key could be both an object and a primitive (e.g.,
useranduser.name). - Invalid Indices: Non-numeric values inside brackets (e.g.,
hobbies[abc]), which should be treated as object keys.
Step-by-Step Guide to Unflattening#
The unflattening process involves three core steps:
- Iterate over the flattened object to process each key-value pair.
- Tokenize the key into nested parts (e.g.,
details.hobbies[0]→['details', 'hobbies', 0]). - Build the nested structure by traversing the tokenized parts and creating objects/arrays as needed.
Step 1: Iterate Over Key-Value Pairs#
Use Object.entries() to loop through each key-value pair in the flattened object.
Step 2: Tokenize the Key#
Split the key into meaningful parts (tokens) to identify nested objects and arrays. For example, details.hobbies[0] should split into:
- Object key:
details - Object key:
hobbies - Array index:
0
We’ll use a regular expression to parse keys and array indices:
([^\[\.]+): Matches object keys (sequences of characters excluding dots and brackets).\[(\d+)\]: Matches array indices (digits inside brackets).
Step 3: Build the Nested Structure#
Traverse the tokenized parts to construct nested objects and arrays:
- For object keys: Create empty objects if they don’t exist.
- For array indices: Create empty arrays, ensure they have enough elements, and initialize undefined indices.
Handling Arrays in Dot Notation#
Arrays are trickier than objects because they require numeric indices and ordered elements. Here’s how to handle them:
- Identify Array Indices: Use the regex to detect numeric values inside brackets (e.g.,
[0]→ index0). - Initialize Arrays: If a key is followed by an array index (e.g.,
hobbies[0]), initializehobbiesas an array. - Handle Gaps in Indices: If indices are non-sequential (e.g.,
hobbies[2]), fill gaps withundefinedto maintain array length.
Edge Cases and Solutions#
1. Non-Sequential Array Indices#
Example: { 'items[2]': 'orange' }
Solution: Initialize the array with undefined for missing indices: items: [undefined, undefined, 'orange'].
2. Conflicting Keys#
Example: { 'a': 1, 'a.b': 2 }
Solution: Later keys overwrite earlier ones. In this case, a will be 1 (overwriting the object { b: 2 }).
3. Non-Numeric Array Indices#
Example: { 'arr[abc]': 'x' }
Solution: Treat [abc] as an object key: arr: { abc: 'x' } (since abc is not a number).
4. Empty or Invalid Keys#
Example: { '': 'value' }
Solution: Skip invalid keys or throw an error (customize based on use case).
Practical Examples#
Let’s implement the unflatten function step-by-step.
Step 1: Tokenize the Key#
Use a regex to split keys into object keys and array indices:
function tokenizeKey(key) {
const parts = [];
const regex = /([^\[\.]+)|\[(\d+)\]/g; // Matches keys or numeric indices
let match;
while ((match = regex.exec(key)) !== null) {
if (match[1]) parts.push({ type: 'key', value: match[1] }); // Object key
if (match[2]) parts.push({ type: 'index', value: parseInt(match[2], 10) }); // Array index
}
return parts;
}
// Example: tokenizeKey('details.hobbies[0]')
// Returns: [ {type: 'key', value: 'details'}, {type: 'key', value: 'hobbies'}, {type: 'index', value: 0} ]Step 2: Build the Nested Structure#
Iterate over tokens and construct objects/arrays:
function unflatten(flattenedObj) {
const result = {};
for (const [key, value] of Object.entries(flattenedObj)) {
const parts = tokenizeKey(key);
if (parts.length === 0) continue; // Skip invalid keys
let current = result;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isLast = i === parts.length - 1;
if (isLast) {
// Assign value to the final part
if (part.type === 'key') {
current[part.value] = value;
} else { // Array index
if (!Array.isArray(current)) current = []; // Ensure current is an array
const index = part.value;
while (current.length <= index) current.push(undefined); // Fill gaps
current[index] = value;
}
} else {
const nextPart = parts[i + 1];
if (part.type === 'key') {
// Create object/array for the current key
if (nextPart.type === 'index') {
current[part.value] = Array.isArray(current[part.value]) ? current[part.value] : [];
current = current[part.value]; // Move to the array
} else {
current[part.value] = typeof current[part.value] === 'object' && current[part.value] !== null ? current[part.value] : {};
current = current[part.value]; // Move to the object
}
} else { // part.type === 'index'
if (!Array.isArray(current)) current = []; // Ensure current is an array
const index = part.value;
while (current.length <= index) current.push(undefined); // Fill gaps
current[index] = typeof current[index] === 'object' && current[index] !== null ? current[index] : (nextPart.type === 'index' ? [] : {});
current = current[index]; // Move to the array element
}
}
}
}
return result;
}Step 3: Test the Function#
Test with a complex flattened object:
const flattened = {
'name': 'Alice',
'details.age': 25,
'details.hobbies[0]': 'reading',
'details.hobbies[2]': 'gaming', // Non-sequential index
'details.address.city': 'New York',
'scores[0]': 90,
'scores[1]': 85
};
const unflattened = unflatten(flattened);
console.log(unflattened);Output:
{
name: 'Alice',
details: {
age: 25,
hobbies: [ 'reading', undefined, 'gaming' ], // Handles gap at index 1
address: { city: 'New York' }
},
scores: [ 90, 85 ]
}Testing the Unflatten Function#
Validate the function with edge cases:
Test 1: Non-Sequential Array Indices#
unflatten({ 'items[2]': 'orange' });
// { items: [undefined, undefined, 'orange'] }Test 2: Mixed Objects and Arrays#
unflatten({ 'a.b[0].c': 1 });
// { a: { b: [ { c: 1 } ] } }Test 3: Conflicting Keys#
unflatten({ 'user': 'John', 'user.age': 30 });
// { user: 'John' } (later key overwrites the object)Conclusion#
Unflattening JavaScript objects with dot notation and array brackets requires careful handling of nested structures, array indices, and edge cases. By tokenizing keys and building nested objects/arrays incrementally, you can convert flattened data back into its original form.
The provided unflatten function handles most common scenarios, but you may need to adapt it for specific use cases (e.g., stricter validation for array indices or preserving conflicting keys).
References#
- MDN: Object.entries()
- MDN: Regular Expressions
- Lodash.unflatten (alternative library implementation)