Ryan Selk Software Developer

HOME | ABOUT

Dig in Ruby 2.3

I recently upgraded the Ruby version to 2.3 in a large Rails application. The upgrade was uneventful but I found some great spots to use Rubys new dig method. Dig is allows you to attempt to access an element, and if the element does not exist it will return nil.

Although it’s generally something we want to avoid, in some situations you are given a deeply nested structure. This is often the case when interacting with an external API. Lets use an example from SCIM (System for Cross-domain Identity Management).

scim_user.json

{
  "urn:scim:schemas:extension:enterprise:1.0": {
    "employeeNumber": "19231",
    "division": "West",
    "department": "Development",
    "manager": {
      "managerId": "21314",
      "displayName": "Jane Smith"
    }
  }
}

Lets load this json into an in memory ruby hash:

data = JSON.parse(File.read("scim_user.json"))
p data
#  => {"urn:scim:schemas:extension:enterprise:1.0"=>{"employeeNumber"=>"19231", "division"=>"West", "department"=>"Development", "manager"=>{"managerId"=>"21314", "displayName"=>"Jane Smith"}}}

If we want to get the managerId we can get so as follows:

manager_id = data["urn:scim:schemas:extension:enterprise:1.0"]["manager"]["managerId"]
puts managerId
# => 19231

This works fine in this case as we have all the needed data. A problem arises when the json does not include some of the fields which we want. Imagine the json did not include any manager information:

scim_user_no_manager.json

{
  "urn:scim:schemas:extension:enterprise:1.0": {
    "employeeNumber": "19231",
    "division": "West",
    "department": "Development"
  }
}

When we try to access the managerId in the same fashion, an exception is thrown.

data = JSON.parse(File.read("scim_user_no_manager.json"))
manager_id = data["urn:scim:schemas:extension:enterprise:1.0"]["manager"]["managerId"]

# => NoMethodError: undefined method `[]' for nil:NilClass

Here are a few ways of dealing with this problem:

Use a conditional before assignment

We can use a conditional before the assignment to ensure that the hash structure contains the information we want. We rely on short circuit evaluation as we fail out of the condition if anything returns nil.

manager_id = nil
if data
   data["urn:scim:schemas:extension:enterprise:1.0"] && 
   data["urn:scim:schemas:extension:enterprise:1.0"]["manager"] && 
   data["urn:scim:schemas:extension:enterprise:1.0"]["manager"]["managerId"]

  manager_id = data["urn:scim:schemas:extension:enterprise:1.0"]["manager"]["managerId"]
end

puts managerId
# => nil

Rails #try method

Rails has had a built in try method since version 3. This is used often in Rails apps. This method is fairly verbose and passing in the hash access method to the #try method can be a little confusing.

manager_id = data["urn:scim:schemas:extension:enterprise:1.0"].try(:[], "manager").try(:[], "managerId")
# => nil

Ruby #Dig

Finally we get to the new dig method. We simply pass in the nested keys we want to get and it will either fetch it for us, or return nil.

manager_id = data.dig("urn:scim:schemas:extension:enterprise:1.0", "manager", "managerId")
# => nil

The dig method is significantly easier to read then alternative methods. While it is usually best to avoid deeply nested structures, it is often unavoidable. If you are dealing with deeply nested hashes, it’s usually going to be worthwhile to use dig.



blog comments powered by Disqus