Nested JSON Patterns: Structure, Access, and Transform
Working with complex product catalogs taught me these nesting patterns. When building AI translation systems for e-commerce, I've transformed deeply nested product hierarchies, category trees, and variant structures thousands of times.
This guide shares practical patterns for structuring, accessing, and transforming nested JSON. These aren't academic examples - they're real solutions from building enterprise systems.
Understanding JSON Nesting
JSON nesting represents hierarchical relationships. When building product catalogs for PIM systems, understanding nesting depth and structure is crucial.
Nesting levels
// Level 0: Simple flat object
{
"id": "PROD123",
"name": "Wireless Mouse",
"price": 29.99
}
// Level 1: One level of nesting
{
"product": {
"id": "PROD123",
"name": "Wireless Mouse",
"price": 29.99
}
}
// Level 3: Three levels deep
{
"catalog": {
"categories": {
"electronics": {
"peripherals": {
"mice": [
{
"id": "PROD123",
"name": "Wireless Mouse"
}
]
}
}
}
}
}
Types of nesting
Objects within objects:
{
"product": {
"name": "Wireless Mouse",
"specifications": {
"weight": "95g",
"dimensions": {
"length": 10,
"width": 6,
"height": 4
}
}
}
}
Arrays of objects:
{
"products": [
{"id": "P1", "name": "Mouse"},
{"id": "P2", "name": "Keyboard"}
]
}
Mixed nesting (most common in real APIs):
{
"order": {
"id": "ORD123",
"customer": {
"name": "John Doe",
"addresses": [
{
"type": "shipping",
"street": "123 Main St",
"city": "Copenhagen"
}
]
},
"items": [
{
"product": {
"id": "PROD123",
"variants": [
{"sku": "PROD123-BLK", "color": "black"}
]
},
"quantity": 2
}
]
}
}
Nesting depth limits
When building translation workflows in Copenhagen, I learned these practical limits:
- Most APIs: 3-5 levels is comfortable
- JSON parsers: 100+ levels supported, but impractical
- Human readability: 2-3 levels max before confusion sets in
- Performance: Deep nesting (10+ levels) slows parsing
When to Flatten vs Keep Nested Structure
The biggest structural decision when designing JSON: nest or flatten? I've learned when each approach works best.
Use nested structure when
1. Representing hierarchical relationships
// Good: Product variants are clearly children of product
{
"product": {
"id": "PROD123",
"name": "T-Shirt",
"variants": [
{"size": "S", "sku": "PROD123-S"},
{"size": "M", "sku": "PROD123-M"},
{"size": "L", "sku": "PROD123-L"}
]
}
}
2. Grouping related fields
// Good: Address fields grouped logically
{
"customer": {
"name": "John Doe",
"shippingAddress": {
"street": "123 Main St",
"city": "Copenhagen",
"postalCode": "1000",
"country": "Denmark"
},
"billingAddress": {
"street": "456 Oak Ave",
"city": "Copenhagen",
"postalCode": "2000",
"country": "Denmark"
}
}
}
3. Supporting flexible structure
// Good: Custom fields without polluting top level
{
"product": {
"id": "PROD123",
"name": "Widget",
"metadata": {
"customField1": "value1",
"customField2": "value2"
}
}
}
Use flat structure when
1. Database mapping
// Good: Maps directly to SQL table columns
{
"order_id": "ORD123",
"customer_name": "John Doe",
"shipping_street": "123 Main St",
"shipping_city": "Copenhagen",
"billing_street": "456 Oak Ave",
"billing_city": "Copenhagen"
}
2. Translation management systems
When building content translation workflows, I flatten for TMS compatibility:
// Good for translation: Each key is translatable field
{
"product_name": "Wireless Mouse",
"product_description": "Ergonomic wireless mouse",
"product_feature_1": "2.4GHz connection",
"product_feature_2": "1200 DPI"
}
3. Simple data without relationships
// Good: No need for nesting
{
"id": "PROD123",
"name": "Widget",
"price": 29.99,
"stock": 100,
"sku": "WDG-123"
}
Flattening nested structures
When I need to flatten for specific systems:
// Flatten nested object to dot notation
function flattenObject(obj, prefix = '') {
const flattened = {};
Object.keys(obj).forEach(key => {
const value = obj[key];
const newKey = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
Object.assign(flattened, flattenObject(value, newKey));
} else {
flattened[newKey] = value;
}
});
return flattened;
}
// Usage
const nested = {
product: {
name: "Mouse",
specs: {
weight: "95g",
dimensions: { length: 10, width: 6 }
}
}
};
const flat = flattenObject(nested);
// {
// "product.name": "Mouse",
// "product.specs.weight": "95g",
// "product.specs.dimensions.length": 10,
// "product.specs.dimensions.width": 6
// }
Unflattening back to nested
function unflattenObject(flat) {
const nested = {};
Object.keys(flat).forEach(key => {
const parts = key.split('.');
let current = nested;
parts.forEach((part, idx) => {
if (idx === parts.length - 1) {
current[part] = flat[key];
} else {
current[part] = current[part] || {};
current = current[part];
}
});
});
return nested;
}
JSONPath and Accessing Deeply Nested Values
When building translation APIs processing complex product catalogs, accessing deeply nested values is a daily task. JSONPath is like XPath for JSON.
Basic JSONPath syntax
const data = {
"store": {
"products": [
{
"name": "Mouse",
"price": 29.99,
"variants": [
{"sku": "M-BLK", "color": "black"},
{"sku": "M-WHT", "color": "white"}
]
},
{
"name": "Keyboard",
"price": 79.99
}
]
}
};
// JSONPath expressions
$.store.products[0].name // "Mouse"
$.store.products[*].name // ["Mouse", "Keyboard"]
$.store.products[*].price // [29.99, 79.99]
$.store.products[?(@.price < 50)] // Products under $50
$..variants // All variants anywhere in structure
$.store.products[0].variants[*].sku // ["M-BLK", "M-WHT"]
Using JSONPath library
npm install jsonpath
const jp = require('jsonpath');
// Find all products over $50
const expensive = jp.query(data, '$.store.products[?(@.price > 50)]');
// Get all SKUs in the entire catalog
const skus = jp.query(data, '$..sku');
// Find products with variants
const withVariants = jp.query(data, '$.store.products[?(@.variants)]');
Safe nested access with optional chaining
JavaScript optional chaining prevents errors with undefined paths:
// Without optional chaining - crashes if undefined
const sku = data.store.products[0].variants[0].sku;
// With optional chaining - returns undefined safely
const sku = data.store?.products?.[0]?.variants?.[0]?.sku;
// With default value
const sku = data.store?.products?.[0]?.variants?.[0]?.sku ?? 'N/A';
lodash get() for safe access
When building content translation systems, I use lodash for reliable nested access:
const _ = require('lodash');
// Safe access with default value
const sku = _.get(data, 'store.products[0].variants[0].sku', 'N/A');
// Access with array notation
const name = _.get(data, ['store', 'products', 0, 'name']);
// Check if path exists
const hasVariants = _.has(data, 'store.products[0].variants');
jq for command-line JSON processing
For processing large product catalogs via scripts:
# Get all product names
cat catalog.json | jq '.store.products[].name'
# Filter products over $50
cat catalog.json | jq '.store.products[] | select(.price > 50)'
# Extract nested SKUs
cat catalog.json | jq '.store.products[].variants[]?.sku'
# Transform structure
cat catalog.json | jq '.store.products[] | {name, price, skus: [.variants[]?.sku]}'
Transforming Nested Structures Efficiently
When translating product data across languages, I transform nested structures constantly. Here are the patterns that work in production.
Recursive transformation
// Transform all string values (e.g., for translation)
function transformStrings(obj, transformer) {
if (typeof obj === 'string') {
return transformer(obj);
}
if (Array.isArray(obj)) {
return obj.map(item => transformStrings(item, transformer));
}
if (obj && typeof obj === 'object') {
const result = {};
Object.keys(obj).forEach(key => {
result[key] = transformStrings(obj[key], transformer);
});
return result;
}
return obj;
}
// Usage: Uppercase all strings in nested structure
const translated = transformStrings(productData, str => str.toUpperCase());
Map nested arrays
// Transform product variants
const products = {
items: [
{
id: "P1",
name: "T-Shirt",
variants: [
{size: "S", price: 19.99},
{size: "M", price: 19.99},
{size: "L", price: 21.99}
]
}
]
};
// Add 20% to all variant prices
const updated = {
...products,
items: products.items.map(product => ({
...product,
variants: product.variants.map(variant => ({
...variant,
price: Math.round(variant.price * 1.2 * 100) / 100
}))
}))
};
Merge nested structures
When combining translated data with original:
const _ = require('lodash');
const original = {
product: {
id: "PROD123",
name: "Mouse",
specs: {
weight: "95g",
color: "black"
}
}
};
const translation = {
product: {
name: "Souris",
specs: {
color: "noir"
}
}
};
// Deep merge
const merged = _.merge({}, original, translation);
// {
// product: {
// id: "PROD123",
// name: "Souris", // Translated
// specs: {
// weight: "95g", // Preserved
// color: "noir" // Translated
// }
// }
// }
Extract values from nested structure
// Extract all unique values for a field
function extractField(obj, fieldName) {
const values = new Set();
function traverse(current) {
if (current && typeof current === 'object') {
if (current[fieldName] !== undefined) {
values.add(current[fieldName]);
}
Object.values(current).forEach(traverse);
}
}
traverse(obj);
return Array.from(values);
}
// Usage: Get all SKUs in catalog
const allSkus = extractField(catalog, 'sku');
Performance considerations
When processing large product catalogs:
- Avoid repeated deep cloning: Use immutable updates only where needed
- Process in batches: Don't transform 10,000 products at once
- Use Map/Set for lookups: O(1) access vs O(n) array searches
- Memoize expensive operations: Cache transformation results
// Good: Process in batches
async function transformCatalog(products, transformer) {
const batchSize = 100;
const results = [];
for (let i = 0; i < products.length; i += batchSize) {
const batch = products.slice(i, i + batchSize);
const transformed = await Promise.all(
batch.map(product => transformer(product))
);
results.push(...transformed);
}
return results;
}
Common Patterns in REST API Responses
After building translation APIs and integrating with dozens of third-party APIs, I've seen these nesting patterns repeatedly.
Envelope pattern
{
"status": "success",
"data": {
"products": [...]
},
"meta": {
"total": 1000,
"page": 1,
"perPage": 20
}
}
Embedded resources
{
"order": {
"id": "ORD123",
"customer": {
"id": "CUST456",
"name": "John Doe",
"email": "john@example.com"
},
"items": [
{
"product": {
"id": "PROD789",
"name": "Mouse",
"price": 29.99
},
"quantity": 2
}
]
}
}
Reference vs embedded
References (flat):
{
"order": {
"id": "ORD123",
"customerId": "CUST456",
"items": [
{"productId": "PROD789", "quantity": 2}
]
}
}
Embedded (nested):
{
"order": {
"id": "ORD123",
"customer": {
"id": "CUST456",
"name": "John Doe"
},
"items": [
{
"product": {
"id": "PROD789",
"name": "Mouse"
},
"quantity": 2
}
]
}
}
Use references when:
- Data changes frequently (avoid stale embedded data)
- Relationship is many-to-many
- Embedded data would be large
Use embedding when:
- Data is read-only or rarely changes
- Client needs data immediately (avoid extra requests)
- Relationship is one-to-one or one-to-many
Pagination with nested data
{
"products": [...],
"pagination": {
"total": 1000,
"page": 1,
"perPage": 20,
"totalPages": 50,
"links": {
"first": "/api/products?page=1",
"prev": null,
"next": "/api/products?page=2",
"last": "/api/products?page=50"
}
}
}
Libraries for Working with Nested Data
When building content translation systems across Copenhagen-based teams, these libraries saved me countless hours.
lodash
The most comprehensive utility library for nested data:
const _ = require('lodash');
// Safe nested access
_.get(obj, 'path.to.deep.value', defaultValue);
// Safe nested set
_.set(obj, 'path.to.deep.value', newValue);
// Deep clone
const copy = _.cloneDeep(obj);
// Deep merge
_.merge(target, source);
// Find in nested arrays
_.find(products, ['variants[0].color', 'black']);
// Group nested data
_.groupBy(products, 'category.name');
ramda
Functional programming approach:
const R = require('ramda');
// Path-based access
const getPrice = R.path(['product', 'variants', 0, 'price']);
getPrice(data); // 29.99
// Lens for immutable updates
const priceLens = R.lensPath(['product', 'variants', 0, 'price']);
const updated = R.set(priceLens, 34.99, data);
// Transform nested structures
const discountPrices = R.over(
R.lensProp('products'),
R.map(R.evolve({ price: x => x * 0.8 }))
);
jq (command-line)
For processing JSON in scripts:
# Transform nested structure
jq '.products[] | {
id,
name,
variants: [.variants[] | {sku, price}]
}' catalog.json
# Filter and map
jq '.products[] | select(.price > 50) | .name' catalog.json
JSON Schema (for validation)
Define and validate nested structure:
{
"type": "object",
"properties": {
"product": {
"type": "object",
"properties": {
"name": {"type": "string"},
"variants": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"price": {"type": "number", "minimum": 0}
},
"required": ["sku", "price"]
}
}
},
"required": ["name", "variants"]
}
}
}
Nesting Best Practices
These practices come from years of building APIs and processing complex product catalogs.
Limit nesting depth
Good: 2-3 levels
{
"product": {
"name": "Mouse",
"variants": [
{"sku": "M-BLK", "price": 29.99}
]
}
}
Bad: 6+ levels
{
"store": {
"departments": {
"electronics": {
"categories": {
"peripherals": {
"products": {
"mice": [...]
}
}
}
}
}
}
}
Use consistent patterns
Don't mix naming conventions:
// Bad: Inconsistent
{
"productName": "Mouse",
"product_price": 29.99,
"ProductSKU": "M-001"
}
// Good: Consistent camelCase
{
"productName": "Mouse",
"productPrice": 29.99,
"productSku": "M-001"
}
Denormalize for read-heavy workloads
When building translation APIs with frequent reads:
// Good: Embed frequently accessed data
{
"order": {
"id": "ORD123",
"customer": {
"name": "John Doe",
"email": "john@example.com"
}
}
}
Normalize for write-heavy workloads
// Good: References prevent update anomalies
{
"order": {
"id": "ORD123",
"customerId": "CUST456"
}
}
Document nesting structure
Use JSON Schema or OpenAPI to document expected structure:
# OpenAPI example
Product:
type: object
properties:
id:
type: string
name:
type: string
variants:
type: array
items:
$ref: '#/components/schemas/Variant'
Anti-Patterns to Avoid
Mistakes I've seen (and made) in production systems.
Overly deep nesting
// Bad: Too many levels
{
"data": {
"response": {
"result": {
"items": {
"products": {
"list": [...]
}
}
}
}
}
}
// Good: Flatten unnecessary wrappers
{
"products": [...]
}
Mixing arrays and objects inconsistently
// Bad: Single item as object, multiple as array
{
"variant": {"sku": "M-001"}, // One variant
"variants": [ // Multiple variants
{"sku": "K-001"},
{"sku": "K-002"}
]
}
// Good: Always use array for collections
{
"variants": [{"sku": "M-001"}] // Even for single item
}
Repeating data instead of nesting
// Bad: Repeated customer data
[
{
"orderId": "ORD1",
"customerName": "John Doe",
"customerEmail": "john@example.com"
},
{
"orderId": "ORD2",
"customerName": "John Doe",
"customerEmail": "john@example.com"
}
]
// Good: Nest customer, list orders
{
"customer": {
"name": "John Doe",
"email": "john@example.com",
"orders": [
{"id": "ORD1"},
{"id": "ORD2"}
]
}
}
Nesting when flat would work better
// Bad: Unnecessary nesting for translation
{
"product": {
"translations": {
"name": {
"en": "Mouse",
"fr": "Souris"
},
"description": {
"en": "Wireless mouse",
"fr": "Souris sans fil"
}
}
}
}
// Good: Flat structure for translation systems
{
"product_name_en": "Mouse",
"product_name_fr": "Souris",
"product_description_en": "Wireless mouse",
"product_description_fr": "Souris sans fil"
}
Using arrays for non-sequential data
// Bad: Using array index as key
{
"prices": [19.99, 24.99, 29.99] // Which size is which?
}
// Good: Object with named keys
{
"prices": {
"small": 19.99,
"medium": 24.99,
"large": 29.99
}
}
Nested JSON is powerful for representing complex relationships. When building translation systems for product catalogs, I've learned that thoughtful nesting improves readability, maintainability, and performance. The key is choosing the right level of nesting for your use case - not too flat, not too deep, just right.