2705. Compact Object

MediumJavaScript
Leetcode Link

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 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

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

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.

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

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

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

Load More