JSON Transform & Reshape JavaScript: map(), reduce(), jq, JSONata
Last updated:
JSON transformation reshapes data from one structure to another — Array.map() transforms arrays element-by-element, Object.entries().reduce() rebuilds objects with different keys, and JSONata provides a declarative query language for complex restructuring. The most common transformation is flattening nested JSON: Object.entries(nested).flatMap(([k, v]) => typeof v === 'object' ? Object.entries(v).map(([k2, v2]) => [`${k}.${k2}`, v2]) : [[k, v]]) converts { a: { b: 1 } } to { "a.b": 1 }. JSONata expression Account.Order.Product.Price extracts nested values without writing a single line of imperative code. This guide covers Array.map() and Array.flatMap() for array reshaping, Object.entries()/Object.fromEntries() for object restructuring, flattening and unflattening nested JSON, jq transformations with map() and to_entries, JSONata declarative transformations, and Zod's .transform() for type-safe reshaping.
Array.map(): Element-by-Element Transformation
Array.map()is the primary tool for transforming JSON arrays — it returns a new array of the same length, with each element replaced by the callback's return value. BLUF: map transforms shape, not length. Use it to rename fields, compute derived values, and reformat data types across every element without a loop.
const users = [
{ user_id: 1, user_name: "Alice", created_ts: 1700000000 },
{ user_id: 2, user_name: "Bob", created_ts: 1700086400 },
];
// Rename fields and derive new values
const normalized = users.map(item => ({
id: item.user_id,
name: item.user_name,
createdAt: new Date(item.created_ts * 1000).toISOString(),
}));
// [{ id: 1, name: "Alice", createdAt: "2023-11-14T..." }, ...]
// Destructuring rename — concise for fixed key sets
const renamed = users.map(({ user_id: id, user_name: name, ...rest }) => ({
id, name, ...rest,
}));
// Index is the second argument — useful for adding position
const withIndex = users.map((item, i) => ({ ...item, position: i + 1 }));
// Async map — run serial or parallel
const withProfile = await Promise.all(
users.map(async user => ({
...user,
profile: await fetchProfile(user.user_id),
}))
);
// Map over nested arrays (two-level)
const orders = [
{ orderId: 101, items: [{ sku: "A1", qty: 2 }, { sku: "B3", qty: 1 }] },
];
const flattened = orders.map(order => ({
...order,
items: order.items.map(item => ({ ...item, total: item.qty * getPriceFor(item.sku) })),
}));map() never mutates the original array and always returns an array of the same length as the input. If you need to change the length (filter + transform), use .filter().map() or .flatMap(). For very large arrays (millions of elements), a for loop is faster because it avoids creating intermediate function call frames — benchmark before optimizing, but prefer map() for clarity in typical API data sizes. When using TypeScript, annotate the return type explicitly on the callback: users.map((u): NormalizedUser => ({ ... })) to catch shape mismatches at compile time.
Array.flatMap(): Expand and Flatten
Array.flatMap() is map() followed by a single-level flat() — it lets each element produce zero, one, or multiple output elements. BLUF: use flatMap() when one input row should expand into multiple rows, or when some rows should be dropped entirely (return [] to skip).
const orders = [
{ orderId: 1, tags: ["urgent", "fragile"] },
{ orderId: 2, tags: [] },
{ orderId: 3, tags: ["gift"] },
];
// Expand: one order → one row per tag
const tagRows = orders.flatMap(order =>
order.tags.map(tag => ({ orderId: order.id, tag }))
);
// [{ orderId: 1, tag: "urgent" }, { orderId: 1, tag: "fragile" }, { orderId: 3, tag: "gift" }]
// orderId 2 produces no rows because tags is empty
// Filter + transform in one pass (return [] to skip)
const activeIds = users.flatMap(u => u.active ? [u.id] : []);
// Equivalent to users.filter(u => u.active).map(u => u.id) — but one pass
// Flatten nested arrays of arrays from API response
const pages = [{ results: [1, 2] }, { results: [3, 4] }, { results: [5] }];
const all = pages.flatMap(page => page.results);
// [1, 2, 3, 4, 5]
// Expand object properties into multiple key-value pairs
const obj = { firstName: "Alice", lastName: "Smith", age: 30 };
const pairs = Object.entries(obj).flatMap(([k, v]) =>
typeof v === "string"
? [{ key: k, value: v, type: "string" }]
: [{ key: k, value: v, type: "number" }]
);
// flatMap with index and conditional logic
const products = [
{ id: 1, variants: null },
{ id: 2, variants: [{ color: "red" }, { color: "blue" }] },
];
const variantRows = products.flatMap(p =>
p.variants
? p.variants.map(v => ({ productId: p.id, ...v }))
: [{ productId: p.id, color: "default" }]
);flatMap() only flattens one level deep — if your callback returns arrays of arrays, use flat(2) or flat(Infinity) after map(). For the common ETL pattern of expanding a one-to-many relationship (one order with many line items into a flat table of line items), flatMap() is the idiomatic JavaScript equivalent of SQL's JOIN or Python's itertools chain. Performance-wise, flatMap() allocates fewer intermediate arrays than .map().flat() — they produce identical results but flatMap() is specified as a single pass.
Object.entries() and Object.fromEntries(): Key Reshaping
Object.entries() converts an object to an array of [key, value] pairs, enabling array methods on object data. Object.fromEntries() converts back. BLUF: the pattern Object.fromEntries(Object.entries(obj).map(...)) is the standard idiom for transforming object keys or values without a manual loop.
const config = { api_key: "abc123", max_retries: 3, base_url: "https://api.example.com" };
// Uppercase all keys
const upper = Object.fromEntries(
Object.entries(config).map(([k, v]) => [k.toUpperCase(), v])
);
// { API_KEY: "abc123", MAX_RETRIES: 3, BASE_URL: "https://api.example.com" }
// snake_case → camelCase keys
const toCamel = k => k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
const camel = Object.fromEntries(
Object.entries(config).map(([k, v]) => [toCamel(k), v])
);
// { apiKey: "abc123", maxRetries: 3, baseUrl: "https://api.example.com" }
// Filter out null/undefined values
const clean = Object.fromEntries(
Object.entries(config).filter(([, v]) => v != null)
);
// Transform values: multiply all numeric values by 2
const doubled = Object.fromEntries(
Object.entries(config).map(([k, v]) => [k, typeof v === "number" ? v * 2 : v])
);
// Invert a key-value map (swap keys and values)
const roles = { alice: "admin", bob: "editor", carol: "viewer" };
const byRole = Object.fromEntries(
Object.entries(roles).map(([user, role]) => [role, user])
);
// { admin: "alice", editor: "bob", viewer: "carol" }
// Reduce: convert entries to a lookup map with transformed values
const prices = { apple: "1.50", banana: "0.75", cherry: "3.00" };
const parsed = Object.fromEntries(
Object.entries(prices).map(([fruit, price]) => [fruit, parseFloat(price)])
);
// Object.entries + reduce for grouping
const items = [{ cat: "fruit", name: "apple" }, { cat: "veg", name: "carrot" }, { cat: "fruit", name: "mango" }];
const grouped = items.reduce((acc, { cat, name }) => {
(acc[cat] ??= []).push(name);
return acc;
}, {});
// { fruit: ["apple", "mango"], veg: ["carrot"] }Object.entries() iterates only own enumerable properties — inherited properties from the prototype chain are excluded, which is what you want for plain JSON objects. Key order follows the ES2015 spec: integer keys first (ascending), then string keys in insertion order, then Symbol keys. Object.fromEntries() accepts any iterable of [key, value] pairs — including Map entries, so Object.fromEntries(new Map([["a", 1]])) works. When the same key appears multiple times in the iterable, the last value wins.
Flattening and Unflattening Nested JSON
Flattening converts { a: { b: { c: 1 } } } to { "a.b.c": 1 } using dot-notation keys. Unflattening is the reverse. BLUF: write a recursive reduce for flattening; for unflattening, split each dot-notation key and build the nested object path imperatively.
// ── Flatten: nested object → dot-notation keys ──────────────────────
function flatten(obj, prefix = "", result = {}) {
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date)) {
flatten(v, key, result); // recurse into nested objects
} else {
result[key] = v; // leaf value — write to result
}
}
return result;
}
flatten({ a: { b: { c: 1 }, d: 2 }, e: 3 });
// { "a.b.c": 1, "a.d": 2, e: 3 }
// Arrays are left as-is by default
flatten({ user: { tags: ["admin", "editor"] } });
// { "user.tags": ["admin", "editor"] }
// ── Unflatten: dot-notation keys → nested object ─────────────────────
function unflatten(flat) {
const result = {};
for (const [dotKey, val] of Object.entries(flat)) {
const parts = dotKey.split(".");
let cursor = result;
for (let i = 0; i < parts.length - 1; i++) {
cursor[parts[i]] ??= {};
cursor = cursor[parts[i]];
}
cursor[parts[parts.length - 1]] = val;
}
return result;
}
unflatten({ "a.b.c": 1, "a.d": 2, e: 3 });
// { a: { b: { c: 1 }, d: 2 }, e: 3 }
// ── Flatten with custom separator ─────────────────────────────────────
function flattenSep(obj, sep = ".", prefix = "", result = {}) {
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}${sep}${k}` : k;
if (v && typeof v === "object" && !Array.isArray(v)) {
flattenSep(v, sep, key, result);
} else {
result[key] = v;
}
}
return result;
}
// Flatten for CSV headers — use "__" separator to avoid dot ambiguity
flattenSep({ address: { city: "Paris", zip: "75001" } }, "__");
// { "address__city": "Paris", "address__zip": "75001" }
// ── Array flattening: flat() and flatMap() ────────────────────────────
const nested = [1, [2, 3], [4, [5, 6]]];
nested.flat(); // [1, 2, 3, 4, [5, 6]] — one level
nested.flat(2); // [1, 2, 3, 4, 5, 6] — two levels
nested.flat(Infinity);// [1, 2, 3, 4, 5, 6] — all levels
// Flatten array-of-arrays from paginated API
const pages = [{ data: [1, 2] }, { data: [3, 4] }];
const all = pages.flatMap(p => p.data); // [1, 2, 3, 4]The flatten function above intentionally leaves arrays as leaf values rather than recursing into them — flattening tags: ["a", "b"] into "tags.0": "a", "tags.1": "b" is rarely useful and breaks most downstream consumers. If you need to serialize arrays into dot-notation, handle that as a separate step. For production use, the flat npm package (npm install flat) provides battle-tested flatten/unflatten with edge case handling for empty objects, circular references, and configurable array handling.
jq map() and to_entries for Transformations
jq is a command-line JSON processor that enables shell-level JSON transformations without writing JavaScript. BLUF: map(expr) transforms array elements, to_entries converts objects to key-value arrays for key manipulation, and from_entries converts back — these three compose to cover most transformation needs.
# ── map(): transform each element ────────────────────────────────────
echo '[{"user_id":1,"user_name":"Alice"},{"user_id":2,"user_name":"Bob"}]' \
| jq 'map({ id: .user_id, name: .user_name })'
# [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
# ── Select inside map: filter + transform ─────────────────────────────
echo '[{"id":1,"active":true},{"id":2,"active":false}]' \
| jq 'map(select(.active)) | map(.id)'
# [1]
# ── to_entries: object → [{key, value}] ──────────────────────────────
echo '{"api_key":"abc","max_retries":3}' \
| jq 'to_entries'
# [{"key":"api_key","value":"abc"},{"key":"max_retries","value":3}]
# ── Rename keys: to_entries → map → from_entries ──────────────────────
echo '{"user_id":1,"user_name":"Alice"}' \
| jq 'to_entries | map(.key |= gsub("_(?<x>[a-z])"; .x | ascii_upcase)) | from_entries'
# {"userId":1,"userName":"Alice"}
# ── Filter out null values ────────────────────────────────────────────
echo '{"a":1,"b":null,"c":3}' \
| jq 'to_entries | map(select(.value != null)) | from_entries'
# {"a":1,"c":3}
# ── Expand array field: one row per tag ───────────────────────────────
echo '[{"id":1,"tags":["a","b"]},{"id":2,"tags":["c"]}]' \
| jq '[.[] | {id: .id, tag: .tags[]}]'
# [{"id":1,"tag":"a"},{"id":1,"tag":"b"},{"id":2,"tag":"c"}]
# ── Flatten one level of nesting ──────────────────────────────────────
echo '{"user":{"id":1,"name":"Alice"},"score":42}' \
| jq '[to_entries[] | if .value | type == "object" then .value | to_entries[] else . end] | from_entries'
# {"id":1,"name":"Alice","score":42}
# ── @csv and @tsv: transform JSON array to CSV/TSV ────────────────────
echo '[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]' \
| jq -r '.[] | [.id, .name] | @csv'
# 1,"Alice"
# 2,"Bob"
# ── Pass shell variable into transform ────────────────────────────────
PREFIX="usr_"
echo '[{"id":1},{"id":2}]' \
| jq --arg p "$PREFIX" 'map({id: ($p + (.id | tostring))})'
# [{"id":"usr_1"},{"id":"usr_2"}]jq's walk(f) function (available in jq 1.6+) applies a transformation recursively to every value in the JSON tree — use walk(if type == "object" then with_entries(.key |= gsub("_"; "-")) else . end) to rename keys at all nesting levels. The env object gives access to environment variables: env.API_KEY without the --arg flag. For bulk file processing, use jq -s to slurp all input into a single array, or process line by line with jq -c (compact output, one JSON per line) feeding into another command.
JSONata: Declarative JSON Transformation
JSONata is a declarative query and transformation language for JSON — the equivalent of XPath for XML, but with aggregation, string functions, and object construction built in. BLUF: a JSONata expression like Account.Order.Product.Price traverses nested arrays automatically; wrap field references in { "key": expr } to construct a new object shape.
// npm install jsonata
const jsonata = require("jsonata");
const data = {
Account: {
Name: "Firefly",
Order: [
{ OrderID: "order1", Product: [{ Description: "Widget", Price: 9.99, Quantity: 2 }] },
{ OrderID: "order2", Product: [{ Description: "Gadget", Price: 24.99, Quantity: 1 }] },
],
},
};
// ── Path navigation: traverse arrays automatically ────────────────────
const prices = await jsonata("Account.Order.Product.Price").evaluate(data);
// [9.99, 24.99]
// ── Aggregation: $sum, $max, $count ──────────────────────────────────
const total = await jsonata("$sum(Account.Order.Product.Price)").evaluate(data);
// 34.98
const totalRevenue = await jsonata(
"$sum(Account.Order.Product.(Price * Quantity))"
).evaluate(data);
// 44.97
// ── Object construction: reshape with { } ────────────────────────────
const summary = await jsonata(
"Account.Order.Product.{ "name": Description, "unitPrice": Price, "qty": Quantity, "lineTotal": Price * Quantity }"
).evaluate(data);
// [{ name: "Widget", unitPrice: 9.99, qty: 2, lineTotal: 19.98 }, ...]
// ── Conditional expressions ───────────────────────────────────────────
const discounted = await jsonata(
"Account.Order.Product.{ "name": Description, "price": Price > 10 ? Price * 0.9 : Price }"
).evaluate(data);
// ── String functions ──────────────────────────────────────────────────
const upper = await jsonata("$uppercase(Account.Name)").evaluate(data);
// "FIREFLY"
// ── Merge arrays of objects ───────────────────────────────────────────
const flat = await jsonata(
"Account.Order.{ "orderId": OrderID, "products": Product }"
).evaluate(data);
// ── $map() and $filter() ──────────────────────────────────────────────
const expensive = await jsonata(
"$filter(Account.Order.Product, function($p) { $p.Price > 10 })"
).evaluate(data);
// ── Bind variables with $$ ────────────────────────────────────────────
const withAccount = await jsonata(
"Account.{ "accountName": Name, "orders": $count(Order) }"
).evaluate(data);
// ── Transform in Node.js with bindings ───────────────────────────────
const expr = jsonata("$sum(items.(price * qty))");
const result = await expr.evaluate({ items: [{ price: 5, qty: 3 }, { price: 2, qty: 10 }] });
// 35JSONata is used in production by IBM App Connect, Node-RED, AWS Step Functions (for some transform states), and multiple integration platform-as-a-service (iPaaS) tools. It handles recursive descent ($.** selects all descendant values), regular expressions ($match(str, /pattern/)), and date/time functions ($now(), $fromMillis()). For data pipelines where the transformation logic changes per customer or environment, JSONata expressions stored in a database are far more maintainable than equivalent JavaScript functions. The online playground at jsonata.org allows testing expressions without any setup.
Zod .transform(): Type-Safe Reshaping
Zod's .transform() runs a synchronous or async function after schema validation succeeds, converting the validated input type to a new output type with full TypeScript inference. BLUF: use .transform() to coerce, rename, or compute derived values in the same step as validation — the output type is automatically inferred, eliminating a separate transformation pass.
import { z } from "zod";
// ── Basic transform: string → number ──────────────────────────────────
const NumericString = z.string().transform(s => parseInt(s, 10));
NumericString.parse("42"); // 42 (number, not string)
// ── Object transform: rename + derive fields ──────────────────────────
const UserSchema = z
.object({
user_id: z.number(),
user_name: z.string(),
status: z.enum(["active", "inactive"]),
})
.transform(data => ({
id: data.user_id,
name: data.user_name,
active: data.status === "active",
}));
type User = z.infer<typeof UserSchema>;
// { id: number; name: string; active: boolean }
UserSchema.parse({ user_id: 1, user_name: "Alice", status: "active" });
// { id: 1, name: "Alice", active: true }
// ── z.preprocess: transform before validation ─────────────────────────
const Coerced = z.preprocess(
val => (typeof val === "string" ? JSON.parse(val) : val),
z.object({ id: z.number() })
);
Coerced.parse('{"id":42}'); // { id: 42 } — parses JSON string first
// ── .pipe(): chain schemas through transforms ─────────────────────────
const JsonString = z
.string()
.transform(s => JSON.parse(s))
.pipe(z.object({ id: z.number(), name: z.string() }));
JsonString.parse('{"id":1,"name":"Alice"}'); // { id: 1, name: "Alice" }
// ── Array transform: normalize each element ───────────────────────────
const ProductsSchema = z.array(
z.object({ product_id: z.string(), price_cents: z.number() })
.transform(p => ({ id: p.product_id, price: p.price_cents / 100 }))
);
// ── Async transform: fetch related data during validation ─────────────
const UserWithRolesSchema = z
.object({ id: z.number() })
.transform(async ({ id }) => ({
id,
roles: await fetchRoles(id),
}));
await UserWithRolesSchema.parseAsync({ id: 1 });
// ── Chain multiple transforms ─────────────────────────────────────────
const Slug = z
.string()
.trim()
.toLowerCase()
.transform(s => s.replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""));
Slug.parse(" Hello World! "); // "hello-world"
// ── superRefine: validate after transform with access to ctx ──────────
const PositiveInt = z
.string()
.transform((s, ctx) => {
const n = parseInt(s, 10);
if (isNaN(n)) { ctx.addIssue({ code: "custom", message: "Not a number" }); return z.NEVER; }
return n;
})
.refine(n => n > 0, "Must be positive");Zod's z.infer<typeof Schema> reflects the transformed output type, not the raw input type — use z.input<typeof Schema> to get the input shape before transformation. When building API route handlers, define separate input schemas (with .transform()) and output schemas (without) so the TypeScript types accurately describe what the route receives vs. what it returns. The z.NEVER sentinel returned from a transform along with a ctx.addIssue() call signals that validation should fail — this is the correct pattern for transforms that can fail (parsing, database lookups). For JSON schema validation beyond runtime type coercion, combine Zod with JSON Schema generation via zod-to-json-schema.
Key Terms
- Array.map()
- A JavaScript Array method that returns a new array with the same length as the original, where each element is the return value of the provided callback function. The callback receives three arguments: the current element, the current index, and the original array.
map()never mutates the original array and always produces an array of the same length — to change length, combine with.filter()or use.flatMap(). In TypeScript, the return type ofmap()is inferred from the callback's return type, enabling safe element-by-element reshaping with compile-time type checking. - Array.flatMap()
- A JavaScript Array method equivalent to calling
.map()followed by.flat(1)— each callback invocation can return an array of zero, one, or multiple elements, and all results are concatenated into a single flat array. Return[]from the callback to skip an element (filter), return a single-element array or value to keep it (map), or return a multi-element array to expand it.flatMap()only flattens one level — for deeper nesting, chain.flat(n)after.map(). It is the standard JavaScript idiom for SQL-style unnesting of array fields into separate rows. - Object.fromEntries()
- A JavaScript static method that constructs a new object from an iterable of
[key, value]pairs — the inverse ofObject.entries(). Accepts any iterable, including arrays, Maps, and generators. When duplicate keys appear in the input iterable, the last pair wins. Introduced in ES2019, it is the standard way to convert a transformed array of entries back into an object:Object.fromEntries(Object.entries(obj).map(([k, v]) => [transform(k), v])). Also useful for converting aMapto a plain object:Object.fromEntries(myMap). - flattening
- The process of converting a nested JSON object into a flat object where nested keys are encoded in the top-level key name, typically using dot notation:
{ a: { b: 1 } }becomes{ "a.b": 1 }. Flattening is used to prepare nested JSON for CSV export, spreadsheet tools, ElasticSearch indexing, and flat key-value stores. The inverse operation (unflattening) reconstructs the nested structure from dot-notation keys. Arrays within nested objects may be left as-is or expanded into indexed keys depending on the use case and the flattening implementation. - JSONata
- A declarative query and transformation language for JSON data, created by IBM and used in Node-RED, IBM App Connect, and AWS integrations. JSONata expressions navigate nested structures with dot notation (
Account.Order.Product), aggregate values with built-in functions ($sum(),$max(),$count()), construct new object shapes with{ "key": expr }syntax, and filter with$filter()or predicate expressions. The npm packagejsonataevaluates expressions asynchronously in Node.js. JSONata is particularly powerful for ETL pipelines where transformation logic must be stored as data (in a database or config file) rather than compiled JavaScript. - Zod .transform()
- A Zod schema method that attaches a transformation function to run after a schema validates its input successfully. The function receives the validated (and typed) input and returns a new value of any type — the output type is reflected in
z.infer<>automatically. Supports synchronous and async transformations (useparseAsync()for async). Returnz.NEVERcombined withctx.addIssue()to signal a transform-time validation failure. The method composes with.pipe()to chain multiple schemas:z.string().transform(JSON.parse).pipe(z.object({...}))parses a JSON string and validates its structure in one expression.
FAQ
How do I transform a JSON array in JavaScript?
Use Array.map() after parsing with JSON.parse(): const result = data.map(item => ({ id: item.user_id, name: item.user_name })). The callback receives each element and returns its new shape. For async transformations (database lookups, API calls per element), use await Promise.all(data.map(async item => ({ ...item, extra: await fetch(item.id) }))). To filter and transform in one pass, use flatMap(): return [] to skip an element, or a transformed value to keep it. Chain .filter().map() for two-pass filter-then-transform when readability matters more than the minor performance cost of iterating twice.
How do I rename keys in a JSON object?
For a fixed set of key renames, use destructuring with rename: const { user_id: id, user_name: name } = item. For renaming all keys programmatically, use Object.fromEntries(Object.entries(obj).map(([k, v]) => [newKey(k), v])). For snake_case to camelCase across an entire array: data.map(obj => Object.fromEntries(Object.entries(obj).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]))). For deep recursive renaming across nested objects, write a recursive function that applies the key transform at every level. The camelcase-keys npm package handles this with camelcaseKeys(obj, { deep: true }).
How do I flatten nested JSON?
Write a recursive function using Object.entries() and reduce: iterate each entry, and if the value is a non-array object, recurse with the key as a prefix; otherwise, write the value to the result with the dot-joined key. The minimal implementation: function flatten(obj, p = "") { return Object.entries(obj).reduce((r, [k, v]) => ({ ...r, ...(v && typeof v === "object" && !Array.isArray(v) ? flatten(v, p ? `${p}.${k}` : k) : { [p ? `${p}.${k}` : k]: v }) }), {}); }. For the inverse (unflatten), split each dot-notation key and build nested objects path by path. For production use, the flat npm package handles edge cases including circular references and configurable delimiters.
How do I convert a JSON array to an object?
Use Array.reduce() to build a keyed lookup object: const byId = data.reduce((acc, item) => { acc[item.id] = item; return acc; }, {}). Or use Object.fromEntries(data.map(item => [item.id, item])) for a concise one-liner. For grouping multiple items under the same key (one-to-many): data.reduce((acc, item) => { (acc[item.category] ??= []).push(item); return acc; }, {}). After conversion, key lookups are O(1) instead of O(n) linear scan — a critical optimization when looking up thousands of IDs against a large dataset. Use a Map when keys are not strings or when insertion order must be preserved.
What is JSONata?
JSONata is a declarative query and transformation language for JSON data, analogous to XPath/XSLT for XML. It lets you extract values with path expressions (Account.Order.Product.Price), aggregate with built-in functions ($sum(...), $count(...), $max(...)), and construct new object shapes with { "key": expr } syntax — all without imperative JavaScript code. Install with npm install jsonata; evaluate with const result = await jsonata(expr).evaluate(data). JSONata is used in IBM App Connect, Node-RED, and integration platforms where transformation rules are stored as configuration rather than code.
How do I use jq to transform JSON?
jq's map(expr) transforms each array element: jq 'map({ id: .user_id, name: .user_name })' renames fields across an array. Use to_entries | map(select(.value != null)) | from_entries to filter out null values from an object. Expand array fields into multiple rows with [.[] | { id: .id, tag: .tags[] }]. Rename keys with to_entries | map(.key |= gsub("_(?<x>[a-z])"; .x | ascii_upcase)) | from_entries for camelCase conversion. Use @csv or @tsv with -r for raw output to convert JSON arrays to CSV files.
How do I transform JSON with Zod?
Chain .transform(fn) onto any Zod schema to run a transformation after validation: z.string().transform(s => parseInt(s, 10)) parses a string to a number. For objects, transform after z.object(): z.object({ user_id: z.number() }).transform(d => ({ id: d.user_id })). Use z.preprocess(fn, schema) to run a function before validation (useful for JSON.parse or type coercion). Chain schemas with .pipe(): z.string().transform(s => JSON.parse(s)).pipe(z.object({ id: z.number() })). The transformed output type is inferred automatically in z.infer<typeof Schema>.
How do I convert snake_case keys to camelCase?
Convert a single key: const toCamel = k => k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()). Apply to all keys of one object: Object.fromEntries(Object.entries(obj).map(([k, v]) => [toCamel(k), v])). Apply recursively to nested objects and arrays: write a deepCamel(val) function that checks Array.isArray(val), then typeof val === "object", applying toCamel at each level. For a production-ready solution, use camelcase-keys: camelcaseKeys(obj, { deep: true }). In jq, use walk(if type == "object" then with_entries(.key |= gsub("_(?<x>[a-z])"; .x | ascii_upcase)) else . end) for recursive camelCase conversion.
Further reading and primary sources
- MDN: Array.prototype.map() — Official MDN documentation for Array.map() with examples and edge cases
- MDN: Array.prototype.flatMap() — Official MDN documentation for Array.flatMap() combining map and flat
- JSONata Documentation — Official JSONata language reference covering path expressions, functions, and object construction
- jq Manual — Complete jq manual covering map, to_entries, from_entries, and all built-in functions
- Zod .transform() Docs — Zod documentation for .transform(), .pipe(), z.preprocess(), and type inference