1

I'm parsing a JSON result into a Ruby hash. The JSON result looks like this:

{
  "records": [
    {
      "recordName": "7DBC4FAD-D18C-476A-89FB-14A515098F34",
      "recordType": "Media",
      "fields": {
        "data": {
          "value": {
            "fileChecksum": "ABCDEFGHIJ",
            "size": 9633842,
            "downloadURL": "https://cvws.icloud-content.com/B/ABCDEF"
          },
          "type": "ASSETID"
        }
      },
      "recordChangeTag": "ii23box2",
      "created": {
        "timestamp": 1449863552482,
        "userRecordName": "_abcdef",
        "deviceID": "12345"
      },
      "modified": {
        "timestamp": 1449863552482,
        "userRecordName": "_abcdef",
        "deviceID": "12345"
      }
    }
  ]
}

I can't guarantee that it'll return with any/all those values, or that each value will be of a certain type (e.g. Array, Hash, string, number), and if I call it incorrectly then I get a crash.

Right now I need the downloadURL for the first item in the 'records' array, or to write it as I might with the Swift library SwiftyJSON (which I'm far more familiar with):

json["records"][0]["fields"]["data"]["value"]["downloadURL"]

I'm wondering what the safest/best/standard way to do this safely in Ruby is. Perhaps I'm thinking about it wrong?

Andrew
  • 7,693
  • 11
  • 43
  • 81

3 Answers3

3

In ruby 2.3 and above you can use Hash#dig and Array#dig

json = JSON.parse(...)
json.dig('records', 0, 'fields', 'data', 'value', 'downloadURL')

You'll get nil if any of the intermediate values is nil. If one of the intermediate values doesn't have a dig method, for example if `json['records'][0]['fields'] was unexpectedly an integer this will raise TypeError.

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • 2
    ...and for earlier versions you can write `['records', 0, 'fields', 'data', 'value', 'downloadURL'].reduce(json) { |o,k| o && o[k] }`. – Cary Swoveland Aug 14 '16 at 20:44
1

From the documentation (http://ruby-doc.org/stdlib-2.2.3/libdoc/json/rdoc/JSON.html):

require 'json'

my_hash = JSON.parse('{"hello": "goodbye"}')
puts my_hash["hello"] => "goodbye"

If you're worried that you might not have some data. See this question: Equivalent of .try() for a hash to avoid "undefined method" errors on nil?

Community
  • 1
  • 1
James Milani
  • 1,921
  • 2
  • 16
  • 26
1

You can recursively search each object contained in the json object using the recurse_proc method of the JSON module.

Here is an example using the data you provided.

require 'json'

json_string = '{
  "records": [
    {
      "recordName": "7DBC4FAD-D18C-476A-89FB-14A515098F34",
      "recordType": "Media",
      "fields": {
        "data": {
          "value": {
            "fileChecksum": "ABCDEFGHIJ",
            "size": 9633842,
            "downloadURL": "https://cvws.icloud-content.com/B/ABCDEF"
          },
          "type": "ASSETID"
        }
      },
      "recordChangeTag": "ii23box2",
      "created": {
        "timestamp": 1449863552482,
        "userRecordName": "_abcdef",
        "deviceID": "12345"
      },
      "modified": {
        "timestamp": 1449863552482,
        "userRecordName": "_abcdef",
        "deviceID": "12345"
      }
    }
  ]
}'

json_obj = JSON.parse(json_string)
JSON.recurse_proc(json_obj) do |obj|
  if obj.is_a?(Hash) && obj['downloadURL']
    puts obj['downloadURL']
  end
end

Update Based on Frederick's answer and Cary's comment

I originally assumed you just wanted to find the downloadURL somewhere in the json without crashing, but based on Frederick's answer and Cary's comment, it's reasonable to assume that you only want to find the downloadURL if it is at the exact path, rather than if it just exists. Building on Frederick's answer and Cary's comment here are a couple of other options that should safely find the downloadURL at the expected path.

path = ['records', 0, 'fields', 'data', 'value', 'downloadURL']
parsed_json_obj = JSON.parse(json_string)
node_value = path.reduce(parsed_json_obj) do |json,node|
  if json.is_a?(Hash) || (json.is_a?(Array) && node.is_a?(Integer))
    path = path.drop 1
    json[node]
  else
    node unless node == path.last 
  end
end

puts node_value || "not_found"

path = ['records', 0, 'fields', 'data', 'value', 'downloadURL']
begin
  node_value = parsed_json_obj.dig(*path)
rescue TypeError
  node_value = "not_found"
end

puts node_value || "not_found"

BTW, this assumes the json is at least valid, if that is not a given you might want to wrap the JSON.parse in a begin-rescue-end block as well.

nPn
  • 16,254
  • 9
  • 35
  • 58