1

I want to deep copy a json object in Ruby. However when I call clone the json object it doesn't seem to do a deep copy. Is it possible to or am I doing something wrong. Here is the relevant snippet of code of what I am doing now:

idFile = File.new(options[:idFile])
idFile.each_line do |id|
    jsonObj = getJson(id)
    copyObj = jsonObj.clone
    copyObj['details']['payload'] = Base64.decode64(copyObj['payload'])
    copyObj['key'] = 1
    jsonObj['details']['payload'] = Base64.decode64(jsonObj['payload'])
    jsonObj['key'] = 2
    send(copyObj)
    send(jsonObj)  #error here
end

def getJson(id)
    idData = getData(id)
    idJson = JSON.parse!(idData)
    idJson = idJson['request'][0]
    return idJson
end

The error for me occurs because of the decode calls. The first decode call already decodes the object, and the second one tries to decode the same data again, which errors out in the second send call because at that point the data is gibberish.
How do I deep copy that json object?

Dan McClain
  • 11,780
  • 9
  • 47
  • 67
Niru
  • 1,407
  • 1
  • 28
  • 47

2 Answers2

2

JSON is merely text - and in this case it is assumed that the object can round-trip through JSON serialization.

Thus the simplest approach to go Object->JSON(Text)->Object to obtain a true deep clone; alternatively, deserialize the JSON twice (once for the deep clone, as the two deserializations produce in dependent object graphs). Note that Object here is not JSON, but merely the deserialized representation (e.g. Hashes and Arrays) of the data as a standard Ruby objects.

# Standard "deep clone" idiom using an intermediate serialization.
# This is using JSON here but it is the same with other techniques that walk
# the object graph (such as Marshal) and generate an intermediate serialization
# that can be restored later.
jsonObj = getJson(id)
jsonObj["foo"] = "change something"
json = JSON.generate(jsonObj)
copyObj = JSON.parse(json)

# Or, assuming simple deep clone of original, just deserialize twice.
# (Although in real code you'd only want to get the JSON text once.)
jsonObj = getJson(id)
copyObj = getJson(id)

As noted, clone does not do this serialization/deserialization step, but merely ascribes to shallow Object#clone semantics (actually, there is no Hash#clone, so it uses Object#clone's implementation directly):

Produces a shallow copy of obj—the instance variables of obj are copied, but not the objects they reference ..

user2246674
  • 7,621
  • 25
  • 28
  • Sorry, I don't understand. The getJson(id) call does JSON.parse!(txt). Shouldn't that return a ruby object. The documentation says "Parse the JSON document source into a Ruby data structure and return it." At that point its already a hash object, of which I take a sub Hash object and try to clone that. Ruby hash support cloning according to the documentation. If I print out the class it outputs Hash. – Niru Sep 06 '13 at 01:32
  • It *does* return a ruby object. This is why in the first example we first serialize it back to JSON (`json = JSON.generate ..`) before deserializing it again. Note that Hashes supports *cloning*, but not *deep cloning* (`#clone` should never be a deep clone!). My proposed solution uses Obj->JSON->Obj for the clone. Another option is to use Marshall, such as [shown here](http://stackoverflow.com/questions/8710642/ruby-dup-clone-recursively). Note that both approaches generate an *intermediate* document first. Either JSON, or what Marshall uses. Of course this can be done recursively as well. – user2246674 Sep 06 '13 at 01:33
  • I understood that part. According to your suggestion I did, JSON.parse(jsonObj.to_json)... which fixes the error I had, but that seems messy. Is there something I am missing with regards to deep copying a Hash object in ruby-1.9? Googling around all say that jsonObj.clone should deep copy it. Though I guess its incorrectly named and should be called hashObj for this question... – Niru Sep 06 '13 at 01:38
  • @Niru Then the resources found were incorrect or referred to an extension (such as [deep_dup](http://apidock.com/rails/Hash/deep_dup)) or otherwise different environment. There [is no Hash#clone](http://www.ruby-doc.org/core-1.9.3/Hash.html) in Ruby 1.9.3 so it defers to Object#clone with shallow semantics. – user2246674 Sep 06 '13 at 01:39
  • Ah sorry.... I didn't read your comment entirely, and Isee that shallow copy note at the end now. Google search results mention doing a Marshall anyway... so it seems I have to do it this. Thanks for all the help! – Niru Sep 06 '13 at 01:41
0

If you are just looking to do a deep copy of any arbitrary Ruby object, try deep_dive.

https://rubygems.org/gems/deep_dive

Lord Alveric
  • 401
  • 3
  • 8