Skip to content

Don't mutate hashes and arrays passed to merge! and array! - #625

Open
afurm wants to merge 1 commit into
rails:mainfrom
afurm:fix-merge-and-array-mutating-caller-values
Open

afurm wants to merge 1 commit into
rails:mainfrom
afurm:fix-merge-and-array-mutating-caller-values

Conversation

@afurm

@afurm afurm commented Oct 13, 2026

Copy link
Copy Markdown

Summary

When the current scope is empty, merge! and array! (without a block) adopted the caller's hash or array — or the frozen internal EMPTY_ARRAY — as @attributes. Later writes like json.name ... or json.child! { ... } then mutated that object in place (so a shared defaults hash picked up keys from the template) or raised FrozenError.

This is a regression since 2.15.0: json.array! with no collection (or nil) followed by json.child! stores the frozen empty array and fails.

Changes

  • Route external values through _merge_external_values, which shallow-copies when the merge result would be the caller's object (or EMPTY_ARRAY).
  • Add tests covering mutable/frozen hashes with merge!, and array! + child! after an empty / nil collection.

Test plan

  • bin/test test/jbuilder_test.rb

When the current scope is empty, `merge!` and `array!` (without a block)
stored the caller's hash or array as the builder's attributes. Later
writes like `json.name ...` or `json.child! { ... }` then mutated that
object in place, so e.g. a shared defaults hash merged inside a block
picked up keys from the template, and frozen values raised FrozenError.

Since 2.15.0 this also broke `json.array!` with no collection (or nil)
followed by `json.child!`, because the frozen internal empty array was
stored directly.

Store a shallow copy when an external value would be adopted as is.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant