2

Let say I'm getting back a JSON nested hash (or array of hashes) from an API

@example = {"results" = > {{"poop" => "shoop"},{"foo" => {"shizz" => "fizz", "nizzle"=>"bizzle"}}}

YAML markup for the nested hash above

  - poop: shoop
  - foo:
    shizz: fizz
    nizzle: bizzle

Now lets go make a db entry with ActiveRecord from the hash. This works fine.

Thing.create!(:poop  => @example["results"]["poop"],
                :shizz => @example["results"]["foo"]["shizz"],
                :nizzle=> @example["results"]["foo"]["nizzle"])

But what if 'foo' is empty or nil? For example, if an API result has a "person" hash with "first name","last name" # etc, the "person" hash will usually be empty if there is no data, which means the hashes inside it don't exist.

@example = {"results" = > {{"poop" => "shoop"},{"foo" => nil }}

  Thing.create!(:poop  => @example["results"]["poop"],
                :shizz => @example["results"]["foo"]["shizz"],
                :nizzle=> @example["results"]["foo"]["nizzle"])

  NoMethodError: You have a nil object when you didn't expect it! 
  You might have expected an instance of Array. 
  The error occurred while evaluating nil.[]

What's the best way to handle this?

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
thoughtpunch
  • 1,907
  • 4
  • 25
  • 41

4 Answers4

4

I came accross a nil sensitive Hash#get method a while back.

class Hash
  def get(key, default=nil)
    key.split(".").inject(self){|memo, key_part| memo[key_part] if memo.is_a?(Hash)} || default
  end
end

h = { 'a' => { 'b' => { 'c' => 1 }}}
puts h.get "a.b.c"    #=> 1
puts h.get "a.b.c.d"  #=> nil
puts h.get "not.here" #=> nil

It's pretty handy for this sort of JSON drilling.

Otherwise you have to do stuff like this:

h['a'] && h['a']['b'] && h['a']['b']['c']

And that just sucks.

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • indeed very interesting, how 'exhaust' the memo hash – Marek Příhoda Dec 12 '11 at 19:59
  • slick - that will come in handy – klochner Dec 12 '11 at 20:05
  • So would I make a custom class that extends the system 'Hash' class, or would I monkey patch this 'get' method into 'Hash'? – thoughtpunch Dec 12 '11 at 22:54
  • This is rarely any major harm done by extending classes provided by the standard library. I wouldn't make a gem depend on this method or anything, but its fine to use in your own app. So just roll it into `Hash` and have fun doing `{'a' => 1}.get 'a'` – Alex Wayne Dec 12 '11 at 23:25
3

Ruby 2.3.0 introduced a new method called dig on both Hash and Array that solves this problem entirely.

value = hash.dig(:a, :b)

It returns nil if the key is missing at any level.

user513951
  • 12,445
  • 7
  • 65
  • 82
2

If you're using rails (not sure if it's in ruby 1.9):

h = {"a"=>1}
h.try(:[],"a") #1
h.try(:[],"b") #nil

h2 = {"c"=>{"d"=>1}}
h2.try(:[],"c").try(:[],"d")   #1
h2.try(:[],"a").try(:[],"foo") #nil

# File activesupport/lib/active_support/core_ext/object/try.rb, line 28
def try(*a, &b)
  if a.empty? && block_given?
    yield self
  else
    __send__(*a, &b)
  end
end

okliv
  • 3,909
  • 30
  • 47
klochner
  • 8,077
  • 1
  • 33
  • 45
  • I like this solution for Rails apps, but I'm looking for a more low-level, pure Ruby solution. I will try this, thanks! – thoughtpunch Dec 12 '11 at 23:00
2

I went ahead and started passing all Hash results into a Hashie Mash. That way they behave like Ruby objects and respond to nils like a champ!

okliv
  • 3,909
  • 30
  • 47
thoughtpunch
  • 1,907
  • 4
  • 25
  • 41