deep_cloneable: cloning Rails records with the associations you name
This gem gives every ActiveRecord::Base object the possibility to do a deep clone that includes user specified associations.
At a glance
- What is it?
- deep_cloneable is a gem that gives every ActiveRecord::Base object a deep_clone method, copying a record together with the associations, nested includes, attribute filters and attachment files you ask for. It is a Rails 3 and later upgrade of the older deep_cloning plugin, and its sharpest edges are in the version breaks and the attachment handling.
- Who is it for?
- Adopt deep_cloneable if you need to duplicate a Rails record and the graph of records hanging off it, and you want that duplication described in one options hash rather than in a hand written copy method. Do not adopt it if your duplication needs are simple enough for ActiveRecord's dup, since without options the behaviour is identical, or if you need cloning of exotic association types that the gem does not document.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One method for duplicating a record and its graph
deep_cloneable gives every ActiveRecord::Base object the possibility to do a deep clone that includes user specified associations. It is a Rails 3 and later upgrade of the deep_cloning plugin, and the README states the requirements as Ruby 2 or later with 3.3.5 and 3.4.4 tested, and ActiveRecord 3.1.0 or later with 7.1 and 8.0 tested.
The entry point is a deep_clone method that takes an options hash. The key design decision is what happens without options, and the README is explicit: without options, the behaviour is the same as ActiveRecord's dup method. That framing tells you what the gem is, because it is not trying to replace duplication with a different semantic, it is trying to extend it with the part that is painful by hand, which is walking associations.
Installation is the usual Gemfile line:
gem 'deep_cloneable', '~> 3.2.2'The repository is a conventional gem layout with the code in lib/, the suite in test/, a gemspec, a Rakefile, a CHANGELOG.md, a Rubocop configuration, an Appraisals file with gemfiles/ for testing against several dependency sets, and an init.rb for older Rails conventions.
The audience is application developers who duplicate records as a routine operation, for example to copy a template instance, to fork a configuration object, or to create a new record from an existing one with its children.
Include: single, multiple, nested and conditional
The include option takes an association name, a list of them, or a nested structure that says which associations to copy at each level:
pirate.deep_clone include: { treasures: :gold_pieces }
pirate.deep_clone include: [ :mateys, { treasures: :gold_pieces } ]The nested hash form is the one that matters. It reads as a description of the graph you want copied, with the nesting depth deciding how far down the tree the clone reaches.
Two other keys change the behaviour of the copy. validate: false disables validation when saving the dup, which the README frames as a performance speedup, and that matters because validating a whole graph of cloned records costs a validation pass per record.
Conditional includes are expressed with if and unless lambdas inside the include structure, and they apply at any depth:
pirate.deep_clone include: [
{
treasures: { gold_pieces: { if: lambda{|piece| piece.is_a?(Parrot) } } } },
mateys: { unless: lambda{|matey| matey.is_a?(GoldPiece) }
}
]The condition is evaluated against each candidate record, so the same expression that includes a treasure only when it is a Parrot and excludes a matey when it is a GoldPiece reads as a filter on the copied set rather than on the parent.
The dictionary and the problem of duplicating twice
The dictionary option addresses a real correctness problem rather than a performance one. As the README puts it, the dictionary ensures that models are not duped multiple times when it is associated to nested models, and it does this by storing a mapping of the original object to its duped object.
Without it, a record reachable through two different paths in the association graph is copied twice, and the copy no longer mirrors the original's structure. That matters whenever the graph has shared nodes, which in a Rails schema with join models or through associations is common rather than exotic.
The dictionary is enabled with use_dictionary: true, and it can also be prefilled, which is the more interesting option because it lets you control what gets reused:
dictionary = { mateys: {} }
pirate.mateys.each{|m| dict[:mateys][m] = m.deep_clone }
pirate.deep_clone include: [ :mateys, { treasures: [ :matey, :gold_pieces ] } ], dictionary: dictionaryA prefilled dictionary lets you supply copies you have already made, so the clone reuses them rather than producing new ones. The cost is that the caller now owns the identity of the copied records, which is a trade worth making deliberately rather than by accident.
only and except for attributes
Attributes are handled by two options that mirror each other. except names the attributes that should not be copied, only names the attributes that should be copied, and both take a single name, a list, or a nested form that reaches into associated records:
pirate.deep_clone except: :name
pirate.deep_clone except: [ :name, :nick_name ]
pirate.deep_clone include: :parrot, except: [ :name, { parrot: [ :name ] } ]
pirate.deep_clone only: :name
pirate.deep_clone only: [ :name, :nick_name ]
pirate.deep_clone include: :parrot, only: [ :name, { parrot: [ :name ] } ]The nested syntax is the part with a learning curve, because the same hash that describes which associations to include also carries the per level attribute filter. Reading it as a tree helps: the keys at each level are associations, and the only or except list under an association applies to the records reached through it.
The practical use is the classic one. A record usually has a name and a couple of timestamps you want to keep, and an identifier that must be regenerated, and a created_at that should not be copied literally. Filtering attributes centrally is cleaner than resetting them in a postprocessor, because it happens before the copy is saved rather than after.
Processors and the optional block
Two hooks exist for adjusting the copy after duplication: preprocessor and postprocessor, each a lambda taking the original and the copy.
pirate.deep_clone(include: :parrot, preprocessor: ->(original, kopy) { kopy.cloned_from_id = original.id if kopy.respond_to?(:cloned_from_id) })
pirate.deep_clone(include: :parrot, postprocessor: ->(original, kopy) { kopy.cloned_from_id = original.id if kopy.respond_to?(:cloned_from_id) })The README notes that specifying a postprocessor is essentially the same as specifying an optional block, and shows the block form as well:
pirate.deep_clone include: :parrot do |original, kopy|
kopy.cloned_from_id = original.id if kopy.respond_to?(:cloned_from_id)
endThere is a warning attached to both forms, and it is the detail that will save you an afternoon. Using deep_clone with a processor passes all associated objects that are being cloned to the processor, so the callback fires for the parent and for every associated record in the copy. That is why the examples guard with respond_to?, and why the block is written as a two line conditional rather than a direct assignment. A callback that assumes the receiver is always the parent will corrupt the associated records.
Timing matters too, and changed in version 3. Under v2 the optional block was evaluated before deep_cloneable performed its changes, and in v3 it is evaluated after all processing has been done, just before the copy is about to be returned.
Attachments need explicit handling per library
File attachments are the part of cloning that no generic association walk can solve, because the file lives outside the database, and the README covers three libraries separately.
For Carrierwave, associated files are not transferred automatically and the fix is to set the file attribute explicitly in a clone block:
pirate.deep_clone include: :parrot do |original, kopy|
kopy.thumbnail = original.thumbnail
endShrine needs more care. You have to clear the image_data hash attribute from the copy, otherwise Shrine will think there is an old attachment to clean up and will delete the original image. The README shows the pattern, clearing the attribute and then assigning the image itself:
pirate.deep_clone include: [:photos, :parrot] do |original, kopy|
if kopy.is_a?(Photo)
kopy.image_data = nil
kopy.image = original.image
end
endThe note under that example is the important part. You are not assigning image_data directly, because that would result in both records sharing the same asset in your storage, and assigning kopy.image to original.image ensures you use your usual uploading mechanism, such as background jobs.
ActiveStorage offers two options, a full copy or sharing data blobs between two records, and the README gives both shapes. The full copy attaches the file explicitly, downloading the original and passing an io, a filename and a content type, which keeps the copy independent of the original. The has_many_attached variant loops over the attachments and attaches them one by one. Sharing blobs is cheaper and avoids the copy, at the price of the two records sharing storage.
Version breaks and where deep_cloneable is the wrong tool
The upgrade notes are the part of the README to read first, because both major transitions changed behaviour in ways that are not obvious from the method signature.
Coming from v1, the dup method with arguments was replaced in deep_cloneable 2 by the method deep_clone, so every call site changes shape. Coming from v2, there are two breaking changes. The optional block used to be evaluated before deep_cloneable had performed its changes, and in v3 it is evaluated after all processing is done, just before the copy is about to be returned. And when a defined association is not found, the gem raises an exception whose class moved namespace, from ActiveRecord::Base::DeepCloneable::AssociationNotFoundException to DeepCloneable::AssociationNotFoundException, which means rescue clauses referencing the old constant need updating.
Two limits decide whether the gem is the right tool at all. The first is that it is for ActiveRecord models and only ActiveRecord models, so a code base that also uses other persistence layers gets no help for those. The second is that the behaviour without options is exactly ActiveRecord's dup, which means the gem's value is entirely in the options, and a duplication that only needs one has_one association is probably better written by hand where it can also handle the file and validation rules explicitly.
There is also an operational note. The repository has no GitHub releases, so there is no tag to pin, and the Gemfile version constraint in the README asks for the 3.2 series. The last push was on 2026-10-09, and the CHANGELOG.md in the tree is where the release history lives.
Editorial conclusion
Adopt deep_cloneable if you need to duplicate a Rails record and the graph of records hanging off it, and you want that duplication described in one options hash rather than in a hand written copy method. Do not adopt it if your duplication needs are simple enough for ActiveRecord's dup, since without options the behaviour is identical, or if you need cloning of exotic association types that the gem does not document. Verify three things first: which version you install, because the v2 and v1 upgrade notes describe two breaking changes each and the exception class moved namespace; that your association names resolve, since a missing association raises DeepCloneable::AssociationNotFoundException rather than silently skipping; and that your attachment library is handled explicitly, because Carrierwave, Shrine and ActiveStorage each need their own copy or share step. The licence is MIT, there are no GitHub releases to pin, and the last push was on 2026-10-09.
Frequently asked questions
What does the deep_cloneable gem add to ActiveRecord?
It gives every ActiveRecord::Base object a deep_clone method that copies the record together with the associations you name. Without options the behaviour is the same as ActiveRecord's dup, so the gem only adds the graph walking.
How do I install deep_cloneable and what does it require?
Add gem 'deep_cloneable', '~> 3.2.2' to your Gemfile and run bundle install. The README states the requirements as Ruby 2 or later with 3.3.5 and 3.4.4 tested, and ActiveRecord 3.1.0 or later with 7.1 and 8.0 tested.
What is the dictionary option for in deep_cloneable?
It ensures that models are not duped multiple times when they are associated to nested models, by storing a mapping of the original object to its duped object. You enable it with use_dictionary: true or pass a prefilled dictionary to control which copies get reused.
How do I clone records that have files attached?
Each attachment library needs its own handling. Carrierwave files must be assigned explicitly in a clone block, Shrine copies must have image_data cleared or the original image gets deleted, and ActiveStorage supports either a full copy by attaching the downloaded file or sharing the data blobs between records.
What changed when upgrading to deep_cloneable v3?
Two breaking changes. The optional block is now evaluated after all processing is done, just before the copy is returned, where it used to run before deep_cloneable made its changes, and the exception raised for a missing association moved from ActiveRecord::Base::DeepCloneable::AssociationNotFoundException to DeepCloneable::AssociationNotFoundException.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/moiristo-deep-cloneable)Was this useful?