Facebook Pixel

2705. Compact Object

Problem Description

You are given an object or array obj that needs to be "compacted" by removing all falsy values from it.

A compact object is created by:

  • Removing any keys/properties that contain falsy values
  • Applying this removal process recursively to all nested objects and arrays
  • Treating arrays as objects where the indices serve as keys

A value is considered falsy when Boolean(value) returns false. In JavaScript/TypeScript, falsy values include:

  • false
  • 0
  • "" (empty string)
  • null
  • undefined
  • NaN

The compacting process works as follows:

  • For objects: Remove any key-value pairs where the value is falsy
  • For arrays: Remove falsy elements (which may change indices)
  • For nested structures: Apply the same rules recursively

The input obj is guaranteed to be valid JSON (as if it came from JSON.parse).

Example: If the input is {a: null, b: [false, 1], c: {d: "", e: 2}}, the output would be {b: [1], c: {e: 2}} because:

  • Key a is removed (value is null)
  • In array b, false is removed, keeping only 1
  • In nested object c, key d is removed (value is empty string), keeping only e: 2
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to traverse the entire data structure and make decisions at each level about what to keep and what to remove. Since the structure can be nested to any depth, recursion becomes the natural choice.

Let's think about what we're dealing with:

  1. Primitive values: These can't have "keys" to remove, so we return them as-is if they're truthy
  2. Arrays: We need to filter out falsy elements and process remaining elements recursively
  3. Objects: We need to check each key-value pair, keep only truthy values, and process those values recursively

The recursive pattern emerges from recognizing that at each level, we perform the same operation: check if values are falsy, remove them if they are, and for any remaining values that are objects or arrays, apply the same logic to their contents.

For arrays, filter(Boolean) is a clean way to remove falsy values since Boolean acts as a predicate function that returns false for falsy values. After filtering, we still need to recursively process each remaining element with map(compactObject) because those elements might themselves be objects or arrays containing falsy values.

For objects, we iterate through all entries, and only add a key-value pair to our result if the value is truthy. But before adding, we recursively process the value to ensure any nested structures are also compacted.

The base case of our recursion is when we encounter a non-object value (including null) - these are leaf nodes in our data structure tree that can't be traversed further, so we simply return them if they're truthy or exclude them if they're falsy (handled by the calling context).

Solution Approach

The implementation uses recursion to traverse and compact the data structure. Let's walk through how the solution handles each case:

Base Case - Non-objects and null:

if (!obj || typeof obj !== 'object') {
    return obj;
}

When obj is not an object or is null, we return it as-is. This handles primitive values (numbers, strings, booleans) and null. Since these values don't have nested properties, there's nothing to compact.

Array Processing:

if (Array.isArray(obj)) {
    return obj.filter(Boolean).map(compactObject);
}

For arrays, we apply a two-step process:

  1. filter(Boolean) removes all falsy elements. The Boolean constructor acts as a predicate that returns false for falsy values (0, "", false, null, undefined, NaN)
  2. map(compactObject) recursively processes each remaining element to ensure nested structures within the array are also compacted

Object Processing:

return Object.entries(obj).reduce((acc, [key, value]) => {
    if (value) {
        acc[key] = compactObject(value);
    }
    return acc;
}, {} as Obj);

For regular objects, we:

  1. Use Object.entries(obj) to get an array of [key, value] pairs
  2. Use reduce to build a new object, starting with an empty object {}
  3. For each key-value pair:
    • Check if the value is truthy with if (value)
    • If truthy, recursively call compactObject(value) to process nested structures
    • Add the compacted value to the accumulator object with the same key
  4. Return the accumulated object containing only keys with truthy values

The recursion ensures that the compacting process applies to all levels of nesting. Each recursive call either:

  • Returns a primitive value unchanged (base case)
  • Returns a compacted array with all falsy elements removed
  • Returns a compacted object with all falsy-valued keys removed

This depth-first approach guarantees that by the time a value is checked for truthiness at any level, all of its nested content has already been compacted.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through a concrete example to see how the recursive solution works:

Input: {a: 0, b: {c: false, d: "hi"}, e: [1, "", 2]}

Step-by-step execution:

  1. Initial call - compactObject({a: 0, b: {c: false, d: "hi"}, e: [1, "", 2]})

    • Input is an object (not null, not primitive), so we process it as an object
    • We iterate through entries: [["a", 0], ["b", {c: false, d: "hi"}], ["e", [1, "", 2]]]
  2. Processing key "a":

    • Value is 0 (falsy)
    • Skip adding to result (fails if (value) check)
  3. Processing key "b":

    • Value is {c: false, d: "hi"} (truthy - objects are truthy)
    • Recursively call compactObject({c: false, d: "hi"})
      • It's an object, so iterate through entries: [["c", false], ["d", "hi"]]
      • "c" has value false (falsy) → skip
      • "d" has value "hi" (truthy) → recursively call compactObject("hi")
        • It's a primitive string, return "hi"
      • Result: {d: "hi"}
    • Add to result: acc["b"] = {d: "hi"}
  4. Processing key "e":

    • Value is [1, "", 2] (truthy - arrays are truthy)
    • Recursively call compactObject([1, "", 2])
      • It's an array, so:
      • First, filter: [1, "", 2].filter(Boolean)[1, 2] (empty string removed)
      • Then map: [1, 2].map(compactObject)
        • compactObject(1) → returns 1 (primitive)
        • compactObject(2) → returns 2 (primitive)
      • Result: [1, 2]
    • Add to result: acc["e"] = [1, 2]
  5. Final result: {b: {d: "hi"}, e: [1, 2]}

The key observations:

  • Falsy primitive values at the top level of objects/arrays are removed
  • The recursion ensures nested structures are cleaned at all depths
  • Arrays maintain their order but with falsy elements filtered out
  • Objects only keep keys whose values are truthy after recursive processing

Solution Implementation

1def compactObject(obj):
2    """
3    Recursively removes all falsy values from an object or array
4  
5    Args:
6        obj: The input object (dict), list, or any value to compact
7  
8    Returns:
9        A new object/list with all falsy values removed
10    """
11    # Base case: if input is not a dict or list, return as-is
12    if not isinstance(obj, (dict, list)):
13        return obj
14  
15    # Handle list case: filter out falsy values and recursively compact remaining elements
16    if isinstance(obj, list):
17        # Filter out falsy values (None, 0, False, '', [], {}, etc.)
18        # and recursively compact nested objects/lists
19        return [compactObject(item) for item in obj if item]
20  
21    # Handle dict case: iterate through items and keep only truthy values
22    result = {}
23    for key, value in obj.items():
24        # Only add the key-value pair if the value is truthy
25        if value:
26            # Recursively compact the value if it's an object/list
27            result[key] = compactObject(value)
28  
29    return result
30
1import java.util.*;
2import java.util.stream.Collectors;
3
4public class Solution {
5    /**
6     * Recursively removes all falsy values from an object or array
7     * @param obj - The input object or array to compact
8     * @return A new object/array with all falsy values removed
9     */
10    public Object compactObject(Object obj) {
11        // Base case: if input is not a collection/map or is null, return as-is
12        if (obj == null || !(obj instanceof Map || obj instanceof List)) {
13            return obj;
14        }
15      
16        // Handle List case: filter out falsy values and recursively compact remaining elements
17        if (obj instanceof List) {
18            List<?> list = (List<?>) obj;
19            return list.stream()
20                    .filter(this::isTruthy)  // Remove falsy values (null, 0, false, empty string)
21                    .map(this::compactObject)  // Recursively compact nested objects/arrays
22                    .collect(Collectors.toList());
23        }
24      
25        // Handle Map case: iterate through entries and keep only truthy values
26        if (obj instanceof Map) {
27            Map<?, ?> map = (Map<?, ?>) obj;
28            Map<Object, Object> result = new HashMap<>();
29          
30            for (Map.Entry<?, ?> entry : map.entrySet()) {
31                Object key = entry.getKey();
32                Object value = entry.getValue();
33              
34                // Only add the key-value pair if the value is truthy
35                if (isTruthy(value)) {
36                    // Recursively compact the value if it's a Map or List
37                    result.put(key, compactObject(value));
38                }
39            }
40          
41            return result;
42        }
43      
44        return obj;
45    }
46  
47    /**
48     * Helper method to determine if a value is truthy
49     * In Java context: null, 0, false, and empty string are considered falsy
50     * @param value - The value to check
51     * @return true if the value is truthy, false otherwise
52     */
53    private boolean isTruthy(Object value) {
54        if (value == null) {
55            return false;
56        }
57        if (value instanceof Boolean) {
58            return (Boolean) value;
59        }
60        if (value instanceof Number) {
61            return ((Number) value).doubleValue() != 0;
62        }
63        if (value instanceof String) {
64            return !((String) value).isEmpty();
65        }
66        // All other objects (including Maps and Lists) are considered truthy
67        return true;
68    }
69}
70
1#include <variant>
2#include <unordered_map>
3#include <vector>
4#include <memory>
5#include <string>
6
7// Forward declaration for recursive type definition
8struct Obj;
9
10// Type definition for values that can be stored in the object
11// Supports null, bool, int, double, string, nested objects, and arrays
12using Value = std::variant<
13    std::nullptr_t,                           // null
14    bool,                                      // boolean
15    int,                                       // integer
16    double,                                    // floating point
17    std::string,                               // string
18    std::shared_ptr<Obj>,                     // nested object
19    std::shared_ptr<std::vector<Value>>       // array
20>;
21
22// Type definition for a generic object with any key-value pairs
23struct Obj {
24    std::unordered_map<std::string, Value> data;
25};
26
27/**
28 * Helper function to check if a value is considered "falsy"
29 * In JavaScript context: null, undefined, 0, false, "", NaN are falsy
30 * @param value - The value to check
31 * @returns true if the value is truthy, false if falsy
32 */
33bool isTruthy(const Value& value) {
34    return std::visit([](const auto& v) -> bool {
35        using T = std::decay_t<decltype(v)>;
36      
37        // null is falsy
38        if constexpr (std::is_same_v<T, std::nullptr_t>) {
39            return false;
40        }
41        // false is falsy
42        else if constexpr (std::is_same_v<T, bool>) {
43            return v;
44        }
45        // 0 is falsy
46        else if constexpr (std::is_same_v<T, int>) {
47            return v != 0;
48        }
49        // 0.0 and NaN are falsy
50        else if constexpr (std::is_same_v<T, double>) {
51            return v != 0.0 && v == v;  // v == v checks for NaN
52        }
53        // empty string is falsy
54        else if constexpr (std::is_same_v<T, std::string>) {
55            return !v.empty();
56        }
57        // objects and arrays are always truthy (even if empty)
58        else {
59            return true;
60        }
61    }, value);
62}
63
64/**
65 * Recursively removes all falsy values from an object or array
66 * @param obj - The input object or array to compact
67 * @returns A new object/array with all falsy values removed
68 */
69std::shared_ptr<Obj> compactObject(const std::shared_ptr<Obj>& obj) {
70    // Base case: if input is null, return null
71    if (!obj) {
72        return nullptr;
73    }
74  
75    // Create a new object to store the compacted result
76    auto result = std::make_shared<Obj>();
77  
78    // Handle object case: iterate through entries and keep only truthy values
79    for (const auto& [key, value] : obj->data) {
80        // Process the value based on its type
81        std::visit([&](const auto& v) {
82            using T = std::decay_t<decltype(v)>;
83          
84            // Handle array case: filter out falsy values and recursively compact remaining elements
85            if constexpr (std::is_same_v<T, std::shared_ptr<std::vector<Value>>>) {
86                if (v) {
87                    auto compactedArray = std::make_shared<std::vector<Value>>();
88                  
89                    for (const auto& element : *v) {
90                        // Only add truthy values to the compacted array
91                        if (isTruthy(element)) {
92                            // Recursively compact nested objects within the array
93                            std::visit([&](const auto& elem) {
94                                using ElemT = std::decay_t<decltype(elem)>;
95                              
96                                if constexpr (std::is_same_v<ElemT, std::shared_ptr<Obj>>) {
97                                    // Recursively compact nested object
98                                    compactedArray->push_back(compactObject(elem));
99                                } else if constexpr (std::is_same_v<ElemT, std::shared_ptr<std::vector<Value>>>) {
100                                    // Recursively compact nested array (wrap in object for processing)
101                                    auto tempObj = std::make_shared<Obj>();
102                                    tempObj->data["array"] = elem;
103                                    auto compacted = compactObject(tempObj);
104                                    if (compacted && compacted->data.count("array")) {
105                                        compactedArray->push_back(compacted->data.at("array"));
106                                    }
107                                } else {
108                                    // For primitive types, add as-is if truthy
109                                    compactedArray->push_back(elem);
110                                }
111                            }, element);
112                        }
113                    }
114                  
115                    // Add the compacted array to the result
116                    result->data[key] = compactedArray;
117                }
118            }
119            // Handle nested object case
120            else if constexpr (std::is_same_v<T, std::shared_ptr<Obj>>) {
121                if (v) {
122                    // Recursively compact the nested object
123                    result->data[key] = compactObject(v);
124                }
125            }
126            // Handle primitive types
127            else {
128                // Only add the key-value pair if the value is truthy
129                if (isTruthy(value)) {
130                    result->data[key] = v;
131                }
132            }
133        }, value);
134    }
135  
136    return result;
137}
138
1// Type definition for a generic object with any key-value pairs
2type Obj = Record<any, any>;
3
4/**
5 * Recursively removes all falsy values from an object or array
6 * @param obj - The input object or array to compact
7 * @returns A new object/array with all falsy values removed
8 */
9function compactObject(obj: Obj): Obj {
10    // Base case: if input is not an object or is null/undefined, return as-is
11    if (!obj || typeof obj !== 'object') {
12        return obj;
13    }
14  
15    // Handle array case: filter out falsy values and recursively compact remaining elements
16    if (Array.isArray(obj)) {
17        return obj
18            .filter(Boolean)  // Remove falsy values (null, undefined, 0, false, '', NaN)
19            .map(compactObject);  // Recursively compact nested objects/arrays
20    }
21  
22    // Handle object case: iterate through entries and keep only truthy values
23    return Object.entries(obj).reduce((accumulator: Obj, [key, value]: [string, any]): Obj => {
24        // Only add the key-value pair if the value is truthy
25        if (value) {
26            // Recursively compact the value if it's an object/array
27            accumulator[key] = compactObject(value);
28        }
29        return accumulator;
30    }, {} as Obj);
31}
32

Time and Space Complexity

The time complexity is O(n), where n is the total number of elements (including nested elements) in the input object structure.

The algorithm visits each element exactly once during the traversal. For objects, it iterates through all key-value pairs using Object.entries(). For arrays, it processes each element with filter() and map(). Since each element is processed once regardless of the depth of nesting, the overall time complexity remains linear relative to the total number of elements.

The space complexity is O(n) for two main reasons:

  1. Recursion stack depth: In the worst case, the recursion depth equals the maximum nesting level of the object structure, which could be O(n) for a deeply nested structure (like a linked list-shaped object).
  2. Output space: The function creates a new compacted object structure. In the worst case where no elements are removed (all values are truthy), the output size equals the input size, requiring O(n) space.

The intermediate operations like Object.entries(), filter(), and map() also create temporary arrays, but these don't exceed the overall O(n) space bound since they operate on one level at a time.

Common Pitfalls

Pitfall 1: Incorrect Handling of Empty Containers

The Problem: A common mistake is not recognizing that empty arrays [] and empty objects {} are truthy in JavaScript/Python, even though they might seem "empty". The current solution correctly preserves them, but developers often mistakenly try to remove them.

Incorrect Implementation:

# Wrong: Trying to remove empty containers
if isinstance(obj, list):
    result = [compactObject(item) for item in obj if item]
    return result if result else None  # DON'T do this!

# Wrong: Checking for empty dict
if value and (not isinstance(value, dict) or value):  # Confusing logic
    result[key] = compactObject(value)

Why It's Wrong:

  • Boolean([]) returns true in JavaScript and bool([]) returns True in Python
  • Boolean({}) returns true in JavaScript and bool({}) returns True in Python
  • These empty containers should be preserved in the output

Correct Approach: The original solution handles this correctly by only checking truthiness of the values themselves, not whether containers are empty after processing.

Pitfall 2: Modifying the Original Object

The Problem: Some implementations might accidentally modify the original object instead of creating a new one.

Incorrect Implementation:

def compactObject(obj):
    if isinstance(obj, dict):
        # Wrong: Modifying original object
        for key in list(obj.keys()):
            if not obj[key]:
                del obj[key]  # Mutating original!
            else:
                obj[key] = compactObject(obj[key])
        return obj

Why It's Wrong:

  • This mutates the input object, which can cause unexpected side effects
  • If the same object is referenced elsewhere in the code, those references will see the changes

Correct Approach: Always create new objects/arrays during the compacting process:

# Correct: Creating new dict
result = {}
for key, value in obj.items():
    if value:
        result[key] = compactObject(value)
return result

Pitfall 3: Not Handling Special Falsy Values Correctly

The Problem: Developers might use explicit checks that miss certain falsy values or incorrectly classify truthy values as falsy.

Incorrect Implementation:

# Wrong: Using explicit checks that miss cases
if value is not None and value != "":  # Misses 0, False, etc.
    result[key] = compactObject(value)

# Wrong: Using len() which doesn't work for all types
if len(str(value)) > 0:  # Converts 0 to "0" which has length > 0
    result[key] = compactObject(value)

Why It's Wrong:

  • Explicit checks often miss edge cases (like NaN, 0, False)
  • Type conversions can change the truthiness evaluation

Correct Approach: Use Python's built-in truthiness evaluation:

if value:  # Correctly identifies all falsy values
    result[key] = compactObject(value)

This naturally handles all falsy values: None, False, 0, 0.0, "", [] (for filtering), etc.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More