JSON Array Filtering: JavaScript, jq, and Python Examples Explained
Quick answer
💡Use Array.prototype.filter in JavaScript with a callback that returns true for items to keep. For null safety, filter(Boolean) removes falsy items. In jq, use [.[] | select(.status == "active")] — wrap in [] to collect the output back into an array. In Python, use a list comprehension: [item for item in data if item['status'] == 'active']. Paste sample JSON into the ToolDock JSON Validator to confirm your data structure before filtering.
Error symptoms
- ✕
filter() returns an empty array even though matching items exist - ✕
TypeError: Cannot read properties of null (reading 'status') during filter - ✕
jq filter returns a stream of values instead of a JSON array - ✕
Nested array filter flattens the parent object unexpectedly - ✕
filter() mutates the original array in the developer's mental model but actually does not - ✕
TypeScript narrows the filtered type incorrectly when using a plain boolean callback
Common causes
- •The filter predicate compares a string field to a number or vice versa, so === never matches
- •Null or undefined elements in the array throw when the predicate accesses a property
- •jq .[] streams values but the caller expects a bracketed JSON array output
- •The field name used in the predicate does not match the actual key in the JSON — capitalization or spelling differs
- •A nested array filter is applied to the wrong level of nesting, skipping the target objects entirely
- •The filter is called before the async fetch completes, so the array is still empty
When it happens
- •Filtering an API response by status field to show only active records in a dashboard
- •Removing null placeholder entries from a JSON list before rendering a UI component
- •Extracting order line items that match a specific SKU from a nested orders array
- •Building a search feature that filters a local JSON cache of products by category or price range
- •Applying TypeScript type guards to narrow a union-typed array to a specific subtype
Examples and fixes
A shipment list API returns records with mixed status values. The goal is to keep only shipments with status 'delivered' and extract their tracking numbers for display.
Filtering active shipments by status field
❌ Wrong
const shipmentData = [
{ id: 'SHP-001', status: 'delivered', tracking: 'TRK9001' },
{ id: 'SHP-002', status: 'in_transit', tracking: 'TRK9002' },
{ id: 'SHP-003', status: null, tracking: null },
{ id: 'SHP-004', status: 'delivered', tracking: 'TRK9004' }
];
// Crashes when status is null — cannot compare null to string
const deliveredTracking = shipmentData
.filter(s => s.status === 'delivered')
.map(s => s.tracking.toUpperCase());
// TypeError: Cannot read properties of null (reading 'toUpperCase')✅ Fixed
const shipmentData = [
{ id: 'SHP-001', status: 'delivered', tracking: 'TRK9001' },
{ id: 'SHP-002', status: 'in_transit', tracking: 'TRK9002' },
{ id: 'SHP-003', status: null, tracking: null },
{ id: 'SHP-004', status: 'delivered', tracking: 'TRK9004' }
];
// Filter by status AND require tracking to be a non-null string
const deliveredTracking = shipmentData
.filter(s => s.status === 'delivered' && typeof s.tracking === 'string')
.map(s => s.tracking.toUpperCase());
console.log(deliveredTracking); // ['TRK9001', 'TRK9004']The original filter passes on status equality but does not guard against null tracking values before the subsequent .map() call. Adding a typeof check for the tracking field inside the filter predicate ensures only records with both a matching status and a valid tracking string proceed to .map(). The fix keeps the two conditions together in filter rather than adding a separate null check inside map, which makes the intent clearer and avoids a runtime TypeError.
Each order object contains an items array. The goal is to find all orders that contain at least one line item with a specific SKU, then return only those orders with their full item list preserved.
Filtering nested order line items by SKU
❌ Wrong
const orderList = [
{ orderId: 'ORD-100', items: [{ sku: 'WIDGET-A', qty: 2 }, { sku: 'GADGET-B', qty: 1 }] },
{ orderId: 'ORD-101', items: [{ sku: 'GADGET-B', qty: 3 }] },
{ orderId: 'ORD-102', items: [{ sku: 'WIDGET-A', qty: 1 }, { sku: 'COMPONENT-C', qty: 5 }] }
];
const targetSku = 'WIDGET-A';
// Wrong: filters at item level, not order level — produces flat list of items
const matchingOrders = orderList.map(order => order.items).filter(item => item.sku === targetSku);✅ Fixed
const orderList = [
{ orderId: 'ORD-100', items: [{ sku: 'WIDGET-A', qty: 2 }, { sku: 'GADGET-B', qty: 1 }] },
{ orderId: 'ORD-101', items: [{ sku: 'GADGET-B', qty: 3 }] },
{ orderId: 'ORD-102', items: [{ sku: 'WIDGET-A', qty: 1 }, { sku: 'COMPONENT-C', qty: 5 }] }
];
const targetSku = 'WIDGET-A';
// Correct: filter at order level using Array.some() to check nested items
const matchingOrders = orderList.filter(
order => order.items.some(item => item.sku === targetSku)
);
console.log(matchingOrders.map(o => o.orderId)); // ['ORD-100', 'ORD-102']The broken version uses .map() to extract the items arrays first, producing an array of arrays, then applies .filter() at the wrong level — the predicate receives an array of items, not an individual item, so item.sku is always undefined. The fixed version keeps filter at the order level and uses Array.some() inside the predicate to check whether any item in the nested array matches the target SKU. This preserves the full order object in the result, including all line items, not just the matching one.
How filter builds a new array from predicates
Array.prototype.filter iterates every element in the source array and calls the callback function with three arguments: the current element, its index, and the array itself. When the callback returns a truthy value, filter includes that element in the new array it is building. When the callback returns a falsy value, filter skips that element. After visiting every element, filter returns the new array. Crucially, filter does not modify the original array — the source remains unchanged. This immutability is intentional and is one of the reasons filter is preferred over a mutating loop in functional programming styles.
The predicate you pass to filter is the entire logic of the operation. For simple equality checks like item.status === 'active', the predicate is a one-liner. For complex conditions — checking multiple fields, validating nested properties, or calling helper functions — the predicate can be a named function passed by reference, which makes the filter call self-documenting at the call site. Writing named predicates also makes them independently testable, which is valuable for business-critical filtering logic.
One behavioral detail that surprises developers is that filter calls the callback on every element in the array including elements at sparse array positions. For a dense array produced by JSON.parse, this is never an issue — parsed arrays are always dense. But if your array was constructed in JavaScript and has holes from delete operations or from Array(n) constructors, filter visits those positions and the callback receives undefined. This is another reason to validate parsed JSON against a schema before processing it.
The return value of filter is always a new array, even if no elements match. An empty array is returned when the predicate returns falsy for every element. This is an important behavioral guarantee — it means you can always chain .map() or .forEach() after .filter() without checking whether the result is null or undefined. However, an empty result does not indicate an error on its own; you may need to check result.length > 0 after filtering to decide whether to show a 'no results' state in your UI.
Testing filter predicates on sample JSON data
When a filter returns an empty array unexpectedly, the first debugging step is to isolate the predicate from the data by logging the field values it compares. Add a console.log inside the predicate callback to print each element's value for the field you are filtering on. This immediately reveals mismatches — a field you expect to be the string 'active' might actually be the string 'Active' with a capital A, or the number 1 instead of a boolean true, or an empty string instead of null.
Type coercion is the most common hidden cause. Strict equality === requires both value and type to match. Comparing the string '1' to the number 1 with === returns false. Comparing the string 'true' to the boolean true with === also returns false. If the filter was previously working and stopped after an API change, check whether the type of the field changed — some APIs switch from boolean status fields to string status enums across versions without updating documentation.
For jq, the equivalent debugging step is to run the select() expression alone and observe what the filter matches. Instead of [.[] | select(.status == "active")], temporarily run .[] | .status to print every status value in the dataset. This shows the exact strings or types present and makes comparison mismatches obvious. If the field is sometimes missing on certain elements, .status? with the optional operator prevents jq from erroring on elements that lack the key.
For Python, print the type and value of the field being compared inside the list comprehension by converting it to a generator expression temporarily. Using (item for item in shipmentData if not isinstance(item.get('status'), str)) reveals which elements do not have a string status field, pointing directly to the problematic records. The dict.get() method is safer than direct key access in Python because it returns None instead of raising a KeyError when the key is absent.
Null-safe filtering with Boolean coercion
JavaScript arrays parsed from JSON can contain explicit null values. When the server represents missing or unset records with null as an array element — rather than omitting the element or using an object with null fields — you need to remove those null entries before processing the rest of the array. The shorthand for this is items.filter(Boolean). This works because Boolean is a function that coerces its argument. When called with null, undefined, 0, empty string, or false, it returns false, so filter skips those elements. When called with any object, non-zero number, or non-empty string, it returns true.
For most production use cases, filter(Boolean) is too broad. It removes null entries but also removes any element that is the number zero, an empty string, or the boolean false — all of which might be legitimate data values. A more precise approach is items.filter(item => item !== null && item !== undefined), which removes only the genuinely absent entries without accidentally dropping zero prices, empty-string codes, or false flags.
For null-safe property access within a filter predicate, optional chaining provides a clean solution. The expression items.filter(item => item?.status === 'active') uses optional chaining to safely return undefined when item is null or when item does not have a status property. Since undefined === 'active' is false, null elements are quietly excluded from the result without throwing a TypeError.
In TypeScript, using optional chaining inside a filter predicate does not automatically narrow the output type. The type of the filtered array is still the original element type including null. To get a properly narrowed type, use a type guard: items.filter((item): item is ActiveShipment => item !== null && item.status === 'active'). The explicit return type annotation on the callback tells TypeScript that the filter output is narrowed to ActiveShipment[], removing null from the type entirely. This makes downstream code type-safe without needing additional null checks.
jq select versus JavaScript filter syntax
jq and JavaScript filter share the same conceptual model — a predicate function that decides whether each element is included — but their syntax and output behavior differ in important ways. In JavaScript, filter always returns a new array. In jq, .[] | select(predicate) produces a stream of individual values, not an array. If you pipe that stream to another jq filter that expects an array, you will get unexpected results or an error. The correct pattern for getting an array output from jq is to wrap the entire expression in array construction brackets: [.[] | select(.status == "active")].
Another difference is how the two handle missing keys. JavaScript accesses item.status on a null element and throws a TypeError. jq accesses .status on an element that lacks the status key and returns null without error. If you need jq to silently skip elements that are missing the filter key, use select(.status? == "active") — the ? operator makes the key access optional and prevents select from throwing on elements where the key is absent.
For complex jq predicates, jq supports and, or, and not operators as well as parentheses for grouping. A filter like select(.status == "active" and .priority >= 2) works exactly as it reads. jq also supports the // alternative operator for supplying defaults: select((.status // "inactive") == "active") treats elements with a null or missing status as 'inactive' before comparing. This prevents null from propagating into the comparison.
Python list comprehensions have their own behavioral difference: they evaluate the condition lazily but produce a list immediately, similar to JavaScript filter. The key syntactic difference is that the condition comes at the end: [item for item in inventoryData if item['status'] == 'active']. For nested filtering equivalent to JavaScript's .some() check, use any(): [order for order in orderList if any(item['sku'] == targetSku for item in order['items'])]. This is both concise and readable, and it short-circuits on the first match just like Array.some() in JavaScript.
Filtering nested arrays across order line items
Nested array filtering is where most developers make mistakes. The core error is applying filter at the wrong level of nesting. If you have an array of orders and each order has an items array, and you want orders that contain a specific SKU, you must filter at the order level using a nested check inside the predicate — not flatten the items first and then filter.
The broken pattern is to call .map() to extract the nested arrays first and then call .filter() on the result. This produces an array of item arrays where the predicate receives an array as its argument, not an individual item. Since an array is never strictly equal to a SKU string, every element fails the predicate and the result is empty. Alternatively, if .flat() is called before .filter(), the original order objects are lost and you get a flat list of line items with no way to trace back which order they belong to.
The correct approach uses Array.some() inside the filter predicate. Calling orderList.filter(order => order.items.some(item => item.sku === targetSku)) keeps the filter at the order level and uses Array.some() to check the nested condition. Array.some() returns true the moment one item matches, so it short-circuits and does not scan the entire items array for every order. The result is an array of complete order objects, not line items, which is almost always what the caller needs.
For deeply nested structures — for example, orders containing shipments containing packages containing items — the chain of .some() calls becomes verbose. In these cases, a recursive predicate function or a flat structure from the beginning is a better design. If the JSON structure forces you to nest .some() more than two levels deep, consider flattening or reshaping the data once at the normalization boundary rather than repeating the deep traversal everywhere it is needed.
Type guard filters for TypeScript type narrowing
TypeScript's type system does not automatically narrow the element type of a filtered array when the filter callback is an ordinary boolean-returning function. If you have inventoryItems of type (Product | null)[] and call inventoryItems.filter(item => item !== null), TypeScript infers the result as (Product | null)[] — it does not remove null from the type. This means downstream code still needs null checks even though you know the array contains no null values at runtime.
To get proper type narrowing, write the filter callback as a type guard function. A type guard is a function with a return type annotation of the form value is SpecificType. For example: const isProduct = (item: Product | null): item is Product => item !== null. Then calling inventoryItems.filter(isProduct) produces a result typed as Product[], with null fully removed from the element type. TypeScript understands that if isProduct returned true for an element, that element must be of type Product.
Type guard filters are particularly valuable when working with discriminated unions. If your API returns an array of events where each event has a type field and a different payload shape per type, filtering by event.type === 'shipment' narrows the downstream type to ShipmentEvent[] when you write the predicate as a type guard. This eliminates casting and type assertions throughout the code that processes the filtered result.
For performance on large JSON arrays — tens of thousands of elements or more — consider whether filter followed by map can be replaced with a single-pass reduce. A reduce loop visits each element once, accumulating both the filtered and transformed results in one pass rather than two. For most datasets this optimization is unnecessary since filter and map are both O(n) and the constant factor is small. But for arrays above 100,000 elements processed on the main thread, combining operations into a single reduce or considering a streaming approach with a library like clarinet for incremental JSON parsing can meaningfully reduce processing time and memory allocation.
Quick fix checklist
- ✓Log the value and typeof each field inside the predicate to catch type mismatches before they cause silent empty results
- ✓Use item?.field === value with optional chaining to safely access properties on potentially null array elements
- ✓Replace filter(Boolean) with filter(item => item !== null && item !== undefined) to avoid dropping zero or false values
- ✓Use Array.some() inside filter predicates when filtering at the parent level based on a nested array condition
- ✓Write type guard functions with item is SpecificType return types to get properly narrowed TypeScript array types after filtering
- ✓Wrap jq select() output in [] brackets to produce a JSON array instead of a bare stream of values
- ✓In Python, use item.get('field') instead of item['field'] inside list comprehension conditions to avoid KeyError on missing keys
- ✓Check result.length after filtering to distinguish between an empty dataset and a bug where the predicate never matched
Related guides
Frequently asked questions
Why does my JSON array filter return an empty array?
Log the field value inside the predicate callback to check for type mismatches. The field might be the string 'Active' while you compare to 'active', or it might be the number 1 while you compare to the boolean true. Strict equality requires matching type and value. Also confirm the array is not empty before filtering by logging its length immediately after parsing.
How do I filter a JSON array in JavaScript?
Use Array.prototype.filter with a callback that returns true for elements to keep. For example: const activeProducts = productData.filter(p => p.status === 'active'). Filter does not modify the original array — it returns a new array containing only the elements for which the callback returned truthy. You can chain .map() directly after .filter() on the result.
How do I filter a JSON array in jq?
Use select() inside an array constructor: [.[] | select(.status == "active")]. The .[] explodes the array into a stream of elements, select() keeps only those that match the condition, and the surrounding [] collects the stream back into a JSON array. Without the brackets, jq outputs individual values separated by newlines rather than a valid JSON array.
How do I filter null values out of a JSON array?
In JavaScript, use items.filter(item => item !== null && item !== undefined) for precise null removal. Using items.filter(Boolean) also works but additionally removes the number zero, empty strings, and false, which may be valid data. For null property values within objects, use optional chaining inside your predicate: items.filter(item => item?.status === 'active').
How do I filter a nested JSON array?
Filter at the parent level using Array.some() inside the predicate to check the nested condition. For example, to find orders containing a specific SKU: orderList.filter(order => order.items.some(item => item.sku === targetSku)). This preserves the full parent object in the result. Do not flatten the nested array before filtering, as that loses the parent context.
Does Array.filter mutate the original array?
No. Array.prototype.filter always returns a new array and leaves the original completely unchanged. This makes it safe to call filter multiple times on the same source array with different predicates. The source array reference and all its elements remain identical after filter runs. This immutability is one of the reasons filter is preferred over a manual loop that removes elements in-place.
How do I narrow types in TypeScript after filtering?
Write the filter callback as a type guard function with a return type annotation of value is SpecificType. For example: const isActiveProduct = (item: Product | null): item is Product => item !== null && item.status === 'active'. Passing this named function to .filter() tells TypeScript the result is Product[], removing null and inactive items from the type. Plain boolean callbacks do not trigger TypeScript's narrowing.
How do I filter a JSON array in Python?
Use a list comprehension: active_items = [item for item in product_data if item.get('status') == 'active']. The get() method is safer than direct key access because it returns None instead of raising a KeyError when the key is absent. For more complex conditions, you can add and or or clauses: [item for item in data if item.get('status') == 'active' and item.get('quantity', 0) > 0].
What is the performance difference between filter and a for loop for large JSON arrays?
For most datasets, filter is fast enough and the readability benefit outweighs any marginal performance difference. For arrays above 100,000 elements, combining filter and map into a single reduce pass eliminates one full iteration over the array. For very large JSON files where memory is a concern, consider streaming parsers like clarinet or oboe.js, which allow filtering during parsing rather than loading the full array into memory first.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-05.