Overview
Hash compaction removes nil values from hash objects, creating cleaner data structures and eliminating unwanted empty entries. Ruby provides the compact
and compact!
methods on Hash objects to perform this operation, with compact
returning a new hash and compact!
modifying the existing hash in place.
The compaction process iterates through key-value pairs and excludes any pair where the value is nil
. Keys associated with nil
values disappear entirely from the resulting hash. Other falsy values like false
, 0
, or empty strings remain unchanged since compaction targets only nil
specifically.
hash = { a: 1, b: nil, c: 3, d: nil, e: 5 }
compacted = hash.compact
# => { a: 1, c: 3, e: 5 }
# Original hash unchanged
puts hash
# => { a: 1, b: nil, c: 3, d: nil, e: 5 }
Hash compaction commonly occurs in data processing pipelines, API response cleaning, form parameter handling, and configuration management. Web applications frequently use compaction to remove empty form fields or filter out unset configuration values.
user_params = {
name: "John",
email: "john@example.com",
phone: nil,
address: nil,
age: 30
}
clean_params = user_params.compact
# => { name: "John", email: "john@example.com", age: 30 }
The compaction operation preserves the original key-value relationships for non-nil values and maintains the hash's internal structure and ordering. Ruby implements compaction efficiently by creating a new hash with only the qualifying entries rather than removing entries one by one.
Basic Usage
The compact
method creates a new hash containing only the key-value pairs where the value is not nil
. The original hash remains unchanged, making this approach safe for use with shared or referenced hash objects.
data = {
name: "Alice",
email: nil,
age: 25,
phone: nil,
city: "Portland"
}
filtered_data = data.compact
# => { name: "Alice", age: 25, city: "Portland" }
# Original data unmodified
puts data.length
# => 5
The compact!
method modifies the hash in place, removing nil-valued entries from the existing object. This method returns the hash itself if changes occurred, or nil
if no nil values existed.
settings = {
theme: "dark",
notifications: true,
beta_features: nil,
auto_save: nil
}
result = settings.compact!
# => { theme: "dark", notifications: true }
puts settings
# => { theme: "dark", notifications: true }
puts result.equal?(settings)
# => true
When no nil values exist in the hash, compact
returns a new hash with identical contents, while compact!
returns nil
to indicate no modifications occurred.
clean_hash = { a: 1, b: 2, c: 3 }
new_hash = clean_hash.compact
puts new_hash == clean_hash
# => true
puts new_hash.equal?(clean_hash)
# => false
modification_result = clean_hash.compact!
puts modification_result
# => nil
puts clean_hash
# => { a: 1, b: 2, c: 3 }
Compaction works with any hash structure, including nested hashes, but only removes nil values at the top level. Nested nil values require separate handling or recursive compaction approaches.
nested_data = {
user: { name: "Bob", email: nil },
settings: nil,
preferences: { theme: nil, language: "en" }
}
compacted = nested_data.compact
# => { user: { name: "Bob", email: nil }, preferences: { theme: nil, language: "en" } }
Hash compaction preserves all other falsy values including false
, 0
, empty strings, and empty arrays. Only nil
values trigger removal during compaction.
mixed_values = {
flag: false,
count: 0,
text: "",
list: [],
nothing: nil,
value: 42
}
result = mixed_values.compact
# => { flag: false, count: 0, text: "", list: [], value: 42 }
Error Handling & Debugging
Hash compaction methods generally operate without raising exceptions, but certain scenarios require careful handling. Frozen hashes prevent the use of compact!
since this method attempts to modify the hash in place.
frozen_hash = { a: 1, b: nil, c: 3 }.freeze
# This works fine - creates new hash
safe_result = frozen_hash.compact
# => { a: 1, c: 3 }
# This raises FrozenError
begin
frozen_hash.compact!
rescue FrozenError => e
puts "Cannot modify frozen hash: #{e.message}"
end
When debugging compaction operations, verify that target values are actually nil
and not other falsy values. Use nil?
checks and explicit comparisons to identify the exact nature of values.
problematic_hash = { a: "", b: false, c: 0, d: nil, e: "nil" }
puts "Before compaction: #{problematic_hash.length}"
result = problematic_hash.compact
puts "After compaction: #{result.length}"
# Only 'd: nil' gets removed
# Debug each value
problematic_hash.each do |key, value|
puts "#{key}: #{value.inspect} (nil? #{value.nil?})"
end
For deep compaction of nested structures, implement recursive compaction with proper error handling for different object types.
def deep_compact(obj)
case obj
when Hash
obj.each_with_object({}) do |(key, value), hash|
compacted_value = deep_compact(value)
hash[key] = compacted_value unless compacted_value.nil?
end
when Array
obj.map { |item| deep_compact(item) }.compact
else
obj
end
end
complex_data = {
users: [
{ name: "Alice", email: nil, active: true },
{ name: "Bob", email: "bob@example.com", active: nil }
],
settings: { theme: nil, lang: "en" },
metadata: nil
}
cleaned_data = deep_compact(complex_data)
# => { users: [{ name: "Alice", active: true }, { name: "Bob", email: "bob@example.com" }], settings: { lang: "en" } }
When compaction produces unexpected results, examine the data types and values present. String representations of nil like "nil"
or "null"
remain in the hash since they are not the nil
object.
# Common debugging scenario
api_response = {
id: 123,
name: "Product",
description: "null", # String, not nil
price: nil, # Actual nil
category: "" # Empty string, not nil
}
# Only removes the price key
cleaned = api_response.compact
# => { id: 123, name: "Product", description: "null", category: "" }
# To handle string representations of null
cleaned_further = cleaned.reject { |k, v| v == "null" || v == "" }
# => { id: 123, name: "Product" }
Production Patterns
Rails applications frequently use hash compaction for parameter filtering, configuration management, and API response cleaning. Integration with strong parameters and controller actions creates cleaner data flow.
# In a Rails controller
class UsersController < ApplicationController
def update
# Remove nil values from permitted parameters
clean_params = user_params.compact
if @user.update(clean_params)
render json: @user.as_json.compact
else
render json: { errors: @user.errors }, status: 422
end
end
private
def user_params
params.require(:user).permit(:name, :email, :phone, :address)
end
end
API serialization often benefits from compaction to reduce payload size and eliminate unnecessary nil fields. Combined with conditional attribute inclusion, this creates flexible response structures.
class ApiSerializer
def self.serialize(object, options = {})
base_attributes = {
id: object.id,
name: object.name,
email: object.email,
phone: object.phone,
created_at: object.created_at
}
# Add optional attributes based on permissions
base_attributes[:admin_notes] = object.admin_notes if options[:include_admin]
base_attributes[:internal_id] = object.internal_id if options[:include_internal]
# Remove nil values for cleaner API responses
base_attributes.compact
end
end
# Usage in API endpoint
user_data = ApiSerializer.serialize(user, include_admin: current_user.admin?)
Configuration management systems use compaction to handle optional settings and environment-specific overrides. This pattern allows flexible configuration inheritance with explicit nil handling.
class ConfigManager
def self.build_config(base_config, environment_overrides = {}, user_overrides = {})
# Merge configurations with precedence
merged_config = base_config
.merge(environment_overrides.compact)
.merge(user_overrides.compact)
# Remove any explicitly set nil values to use defaults
merged_config.compact
end
end
base = {
timeout: 30,
retries: 3,
debug: false,
cache_size: 1000
}
production_overrides = {
debug: nil, # Explicitly disable debug in production
timeout: 60,
cache_size: nil # Use base value
}
config = ConfigManager.build_config(base, production_overrides)
# => { timeout: 60, retries: 3, debug: false, cache_size: 1000 }
Background job processing benefits from compaction when handling variable argument sets and optional parameters. This prevents jobs from attempting to process meaningless nil values.
class EmailNotificationJob
def self.perform_later(user_id, template, options = {})
# Clean options to avoid passing nil values to email service
clean_options = {
subject: options[:subject],
from_name: options[:from_name],
reply_to: options[:reply_to],
tracking_enabled: options[:tracking_enabled]
}.compact
perform_now(user_id, template, clean_options)
end
def self.perform_now(user_id, template, options)
user = User.find(user_id)
# Email service receives only meaningful parameters
EmailService.send_template(user.email, template, options)
end
end
Common Pitfalls
The distinction between compact
and compact!
creates a common source of errors. Developers sometimes expect compact
to modify the original hash or assume compact!
always returns the hash.
data = { a: 1, b: nil, c: 3 }
# Mistake: Expecting compact to modify original
data.compact
puts data
# => { a: 1, b: nil, c: 3 } (unchanged!)
# Correct: Assign the result
data = data.compact
puts data
# => { a: 1, c: 3 }
# Mistake: Expecting compact! to always return the hash
clean_data = { x: 1, y: 2, z: 3 }
result = clean_data.compact!
puts result.inspect
# => nil (no changes made)
# Correct: Check return value or use the original hash
if clean_data.compact!
puts "Hash was modified"
else
puts "No nil values found"
end
Nested hash structures require special attention since standard compaction only operates on the top level. Developers often expect deep compaction behavior without implementing it explicitly.
# Pitfall: Expecting deep compaction
nested = {
user: {
name: "John",
email: nil,
preferences: { theme: nil, notifications: true }
},
settings: nil
}
result = nested.compact
# Only removes top-level nil values
puts result
# => { user: { name: "John", email: nil, preferences: { theme: nil, notifications: true } } }
# The email and theme keys still have nil values
Type confusion occurs when string representations of null values remain in the hash after compaction. API responses and form data often contain string "null" or "nil" values that survive the compaction process.
# Common API response scenario
parsed_json = {
id: 123,
name: "Item",
description: "null", # JSON null became string
category_id: nil, # Actual Ruby nil
tags: "[]" # Empty array became string
}
compacted = parsed_json.compact
# => { id: 123, name: "Item", description: "null", tags: "[]" }
# Additional filtering needed for string nulls
fully_cleaned = compacted.reject { |k, v|
v.nil? || v == "null" || v == "undefined" || (v.is_a?(String) && v.strip.empty?)
}
Frozen hash modification attempts cause runtime errors that can crash applications if not handled properly. This often occurs in configuration objects or cached data structures.
# Configuration loaded once and frozen
CONFIG = { api_key: ENV['API_KEY'], debug: nil, timeout: 30 }.freeze
# Later in the application - this will crash
begin
CONFIG.compact!
rescue FrozenError
# Handle frozen hash properly
CLEAN_CONFIG = CONFIG.compact.freeze
end
Performance assumptions about compaction can lead to problems with large hashes. While generally efficient, compaction creates new hash objects and iterates through all entries.
# Performance pitfall with frequent compaction
large_hash = 100_000.times.each_with_object({}) do |i, hash|
hash["key_#{i}"] = rand < 0.1 ? nil : rand
end
# Inefficient: Repeated compaction in loop
10.times do
large_hash = large_hash.merge("new_key" => nil).compact
end
# Better: Compact once after all modifications
modified_hash = large_hash.dup
10.times { |i| modified_hash["new_key_#{i}"] = nil }
final_hash = modified_hash.compact
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#compact |
None | Hash |
Returns new hash with nil values removed |
#compact! |
None | Hash or nil |
Removes nil values in place, returns hash if modified or nil if unchanged |
Method Behavior Details
Scenario | compact Result |
compact! Result |
Original Hash |
---|---|---|---|
Hash with nil values | New hash without nils | Same hash, nils removed, returns self | Modified (compact! only) |
Hash without nil values | New identical hash | Returns nil | Unchanged |
Empty hash | New empty hash | Returns nil | Unchanged |
Frozen hash | New hash (works) | Raises FrozenError | Unchanged |
Compaction Rules
Value Type | Removed by Compact | Example |
---|---|---|
nil |
Yes | { a: nil } → {} |
false |
No | { a: false } → { a: false } |
0 |
No | { a: 0 } → { a: 0 } |
Empty string "" |
No | { a: "" } → { a: "" } |
Empty array [] |
No | { a: [] } → { a: [] } |
String "nil" |
No | { a: "nil" } → { a: "nil" } |
Common Patterns
# Safe parameter cleaning
clean_params = params.compact
# In-place modification with error handling
begin
hash.compact!
rescue FrozenError
hash = hash.compact
end
# Conditional compaction
result = should_compact? ? data.compact : data
# Chaining with other hash methods
filtered = hash.compact.reject { |k, v| v.empty? }.transform_keys(&:to_sym)
# Deep compaction helper
def deep_compact(hash)
hash.compact.transform_values do |value|
value.is_a?(Hash) ? deep_compact(value) : value
end
end
Performance Characteristics
Operation | Time Complexity | Space Complexity | Notes |
---|---|---|---|
compact |
O(n) | O(k) | Where k = non-nil entries |
compact! |
O(n) | O(1) | Modifies in place |
Deep compaction | O(n*m) | O(k*m) | Where m = nesting depth |
Related Methods
Method | Purpose | Relationship to Compact |
---|---|---|
#reject |
Remove entries by condition | More general than compact |
#select |
Keep entries by condition | Inverse of reject |
#delete_if |
Remove entries in place | Similar to compact! but with block |
#keep_if |
Keep entries in place | Inverse of delete_if |