rubyguides

Hash#merge

hash.merge(*others) -> hash

The merge method combines one collection with one or more others, returning a new hash. When keys overlap between hashes, the value from the later hash wins. This makes merge essential for combining configurations, updating defaults, and building up collections from multiple sources. It is one of the simplest ways to layer settings without mutating the original data.

Signature

hash.merge(*others) → new_hash
hash.merge(*others) { |key, old_value, new_value| block } → new_hash

The first form performs a simple overwrite merge: when the same key appears in both the receiver and an argument, the argument’s value wins. The second form gives you control over the conflict by calling the block whenever a key is present in both the receiver and one of the argument hashes.

Parameters

ParameterTypeDescription
*othersHashOne or more hashes to merge into the receiver. Each must be a Hash object.

Return Value

Returns a new Hash object containing all key-value pairs from the original data and the merged hashes. The original collections remain unchanged.

Basic Usage

The examples below show the three most common patterns: merging two hashes with a conflict, passing multiple arguments at once, and letting the last value win when the same key appears in several sources. Each example keeps the original collections untouched, which is the main reason to choose merge over merge! in most code.

defaults = { theme: 'dark', language: 'en' }
user_prefs = { theme: 'light' }

merged = defaults.merge(user_prefs)
merged # => {:theme=>"light", :language=>"en"}

The original hashes stay intact:

The original hashes stay intact because merge always returns a new object. This is the key difference from merge! (or update), which modifies the receiver in place. When you are layering configuration from multiple sources, the non-mutating form keeps each source clean for reuse and avoids surprising other parts of the code that hold references to the same objects.

defaults  # => {:theme=>"dark", :language=>"en"}
user_prefs # => {:theme=>"light"}

The original collections remain unchanged because merge always returns a new object. This is the key difference from merge! (or update), which modifies the receiver in place. When you are layering configuration from multiple sources, the non-mutating form keeps each source clean for reuse.

You can merge multiple hashes at once:

base = { a: 1 }
overrides = { b: 2 }
local = { c: 3 }

base.merge(overrides, local)
# => {:a=>1, :b=>2, :c=>3}

Merging several hashes at once is equivalent to chaining calls — base.merge(overrides).merge(local) — but the single-call form is more concise and avoids creating an intermediate hash that is immediately discarded. The arguments are processed left to right, so later keys overwrite earlier ones in order.

The last value wins when keys overlap:

h1 = { a: 1, b: 2 }
h2 = { b: 3, c: 4 }
h3 = { b: 5 }

h1.merge(h2, h3)
# => {:a=>1, :b=>5, :c=>4}

When the same key appears in more than one argument, the rightmost hash takes precedence. The receiver’s value for b (2) is overwritten by h2’s value (3), which is then overwritten by h3’s value (5). The chain is deterministic: merge order is left to right.

In-place vs new hash

Ruby provides two ways to merge: one that creates a new hash, and one that modifies in place.

merge creates a new hash

The merge method returns a new hash and leaves the original unchanged:

config = { host: 'localhost', port: 3000 }
new_config = config.merge({ port: 8080 })

config     # => {:host=>"localhost", :port=>3000}
new_config # => {:host=>"localhost", :port=>8080}

This is useful when you want to preserve the original data. Keeping the original data untouched is often the safer choice when the same defaults are reused in several places.

merge! modifies in place

The merge! method (also known as update) modifies the hash directly:

config = { host: 'localhost', port: 3000 }
config.merge!({ port: 8080 })

config # => {:host=>"localhost", :port=>8080}

Use merge! when you intentionally want to update the original data. It is faster since it does not create a new object, but the mutation means later code sees the changed hash — so the choice should be deliberate. In most cases, the non-mutating merge is safer because it avoids surprising other parts of the program that hold a reference to the same hash.

Block form (conflict resolution)

When keys overlap, you can provide a block to resolve conflicts yourself. The block receives three arguments: the key, the value from the original data, and the value from the incoming one.

prices = { apple: 1.00, banana: 0.50 }
sale_prices = { apple: 0.80, banana: 0.40, orange: 0.75 }

prices.merge(sale_prices) { |key, old, new| old * 0.9 }
# => {:apple=>0.9, :banana=>0.36, :orange=>0.75}

The block receives three arguments: the conflicting key, the value from the receiver, and the value from the argument being merged. It is called once for each key that exists in both hashes. When there is no conflict, the block is not invoked, and the key-value pair is added directly from the argument hash.

The following patterns show common block strategies:

# Keep the higher value
h1.merge(h2) { |_k, v1, v2| v1 > v2 ? v1 : v2 }

# Sum numeric values
h1.merge(h2) { |_k, v1, v2| v1 + v2 }

# Collect values into arrays
h1.merge(h2) { |_k, v1, v2| [v1, v2] }

Each of these patterns answers a different question about conflicting keys. The “keep higher” block is useful for merging priority values where the larger number should win. The “sum” block works for counters, scores, or any numeric data where you want to accumulate rather than replace. The “collect into arrays” pattern preserves both values so the caller can decide later.

When there’s no conflict, the block is never called:

h1 = { a: 1 }
h2 = { b: 2 }

h1.merge(h2) { |key, old, new| raise 'Never called' }
# => {:a=>1, :b=>2}

Because the block is only called for conflicting keys, you can safely provide a block that would be problematic for non-conflicting keys — it simply never runs in those cases. This makes the block form a safe way to handle collisions without affecting the normal merge behavior for unique keys. The block runs once per conflict, and only for the keys that appear in both the receiver and the argument. If no keys overlap, the block is never invoked and the merge behaves identically to the no-block form.

Common use cases

Configuration defaults

DEFAULTS = { timeout: 30, retries: 3, debug: false }

user_config = { timeout: 60 }

config = DEFAULTS.merge(user_config)
# => {:timeout=>60, :retries=>3, :debug=>false}

The defaults pattern is one of the most common uses of merge in Ruby applications. A constant or module-level hash holds the default values, and each caller merges its own overrides on top. Because the original defaults hash is never modified, every caller starts from the same clean baseline regardless of how many overrides are applied. This pattern appears in nearly every Ruby library that accepts a configuration options hash.

Merging user input

form_data = { name: '', email: '' }
required_fields = { name: nil, email: nil, phone: nil }

submitted = { name: 'Alice', email: 'alice@example.com' }

form_data.merge(required_fields).merge(submitted)
# => {:name=>"Alice", :email=>"alice@example.com", :phone=>nil}

Chaining merges like this is a simple way to layer required keys, defaults, and user-supplied values in a predictable order. The later hashes still win, so the last call can represent the final submitted data.

Building options hashes

The defaults pattern is a cornerstone of Ruby library design — a constant holds the fallback values, and each caller layers its preferences on top. Because merge returns a new object, the defaults constant stays frozen for every caller, making the pattern thread-safe by default. This approach appears in countless gems, from HTTP clients to ORMs to CLI frameworks.

default_options = {
  verbose: false,
  format: 'json',
  timeout: 30
}

user_options = { verbose: true }

default_options.merge(user_options)
# => {:verbose=>true, :format=>"json", :timeout=>30}

This style is common when a method accepts keyword-like options but wants to keep a default structure nearby. The merged result is easier to inspect than a long chain of conditional assignments.

See Also