62

I'm using net/http to pull in some json data from the Yahoo Placemaker API. After receiving the response I am performing JSON.parse on the response. This gives me a hash that looks like:

{"processingTime"=>"0.001493", "version"=>"1.4.0.526 build 111113", "documentLength"=>"25", "document"=>{"administrativeScope"=>{"woeId"=>"2503863", "type"=>"Town", "name"=>"Tampa, FL, US", "centroid"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}}, "geographicScope"=>{"woeId"=>"2503863", "type"=>"Town", "name"=>"Tampa, FL, US", "centroid"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}}, "localScopes"=>{"localScope"=>{"woeId"=>"2503863", "type"=>"Town", "name"=>"Tampa, FL, US (Town)", "centroid"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}, "southWest"=>{"latitude"=>"27.8132", "longitude"=>"-82.6489"}, "northEast"=>{"latitude"=>"28.1714", "longitude"=>"-82.2539"}, "ancestors"=>[{"ancestor"=>{"woeId"=>"12587831", "type"=>"County", "name"=>"Hillsborough"}}, {"ancestor"=>{"woeId"=>"2347568", "type"=>"State", "name"=>"Florida"}}, {"ancestor"=>{"woeId"=>"23424977", "type"=>"Country", "name"=>"United States"}}]}}, "extents"=>{"center"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}, "southWest"=>{"latitude"=>"27.8132", "longitude"=>"-82.6489"}, "northEast"=>{"latitude"=>"28.1714", "longitude"=>"-82.2539"}}, "placeDetails"=>{"placeId"=>"1", "place"=>{"woeId"=>"2503863", "type"=>"Town", "name"=>"Tampa, FL, US", "centroid"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}}, "placeReferenceIds"=>"1", "matchType"=>"0", "weight"=>"1", "confidence"=>"8"}, "referenceList"=>{"reference"=>{"woeIds"=>"2503863", "placeReferenceId"=>"1", "placeIds"=>"1", "start"=>"15", "end"=>"20", "isPlaintextMarker"=>"1", "text"=>"Tampa", "type"=>"plaintext", "xpath"=>""}}}}

I am able to access elements by doing things like jsonResponse['version'] but I am not able to do jsonResponse.version. Why is this?

Kyle Decot
  • 20,715
  • 39
  • 142
  • 263

10 Answers10

116

Hash does not have dot-syntax for it's keys. OpenStruct does:

require 'ostruct'
hash = {:name => 'John'}
os = OpenStruct.new(hash)
p os.name #=> "John"

NOTE: Does not work with nested hashes.

Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
steenslag
  • 79,051
  • 16
  • 138
  • 171
50

OpenStruct will work well for a pure hash, but for hashes with embeded arrays or other hashes, the dot syntax will choke. I came across this solution, which works well without loading in another gem: https://coderwall.com/p/74rajw/convert-a-complex-nested-hash-to-an-object basic steps are:

data = YAML::load(File.open("your yaml file"))
json_data = data.to_json
mystr = JSON.parse(json_data,object_class: OpenStruct)

you can now access all objects in mystr using dot syntax.

whodabudda
  • 501
  • 4
  • 4
17

Ruby hashes don't work like this natively, but the HashDot gem would work for this.

HashDot allows dot notation syntax use on hashes. It also works on json strings that have been re-parsed with JSON.parse.

require 'hash_dot'

hash = {b: {c: {d: 1}}}.to_dot
hash.b.c.d => 1

json_hash = JSON.parse(hash.to_json)
json_hash.b.c.d => 1
steel
  • 11,883
  • 7
  • 72
  • 109
11

If you don't want to install any gems, you can try to use the Ruby's native Struct class and some Ruby tricks, like the splat operator.

# regular hashes
customer = { name: "Maria", age: 21, country: "Brazil" }
customer.name
# => NoMethodError: undefined method `name' for {:name=>"Maria", :age=>21, :country=>"Brazil"}:Hash

# converting a hash to a struct
customer_on_steroids = Struct.new(*customer.keys).new(*customer.values)
customer_on_steroids.name
#=> "Maria"

Please note that this simple solution works only for single-level hashes. To make it dynamic and fully functional for any kind of Hash, you'll have to make it recursive to create substructs inside your struct.

You can also store the Struct as if it was a class.

customer_1 = { name: "Maria", age: 21, country: "Brazil" }
customer_2 = { name: "João",  age: 32, country: "Brazil" }
customer_3 = { name: "José",  age: 43, country: "Brazil" }

Customer = Struct.new(*customer_1.keys)
customer_on_steroids_1 = Customer.new(*customer_1.values)
customer_on_steroids_2 = Customer.new(*customer_2.values) 
customer_on_steroids_3 = Customer.new(*customer_3.values)

Read more about Ruby Struct class.

vinibrsl
  • 6,563
  • 4
  • 31
  • 44
9

Why not, you can do this via metaprogramming

module LookLikeJSON
  def method_missing(meth, *args, &block)
    if has_key?(meth.to_s)
      self[meth.to_s]
    else
      raise NoMethodError, 'undefined method #{meth} for #{self}' 
    end
  end
end

h = {"processingTime"=>"0.001493", "version"=>"1.4.0.526 build 111113", "documentLength"=>"25"}
h.extend(LookLikeJSON)
h.processingTime #=> "0.001493"
megas
  • 21,401
  • 12
  • 79
  • 130
  • interpolate string in single quote? : / And using `stringify_keys!` to normalize the keys first. – Yuanfei Zhu Mar 06 '12 at 04:49
  • 1
    Perhaps a smaller, single-line version of this: `Hash.define_method(:method_missing) { |*args| m = args.first; fetch(m, nil) || fetch(m.to_s, nil) || super(*args) }` – Henry Blyth Mar 23 '21 at 18:07
6

That is a JavaScript feature, not a Ruby feature. In Ruby, to use a "dot syntax", the object would need to respond to those methods. Ruby hashes use the #[](key) method to access elements.

d11wtq
  • 34,788
  • 19
  • 120
  • 195
3

I'm gonna go ahead and piggyback off @whodabudda with this one liner

# example hash
hash = { some: [ {very: :deep}, {very: :nested}, {very: :hash} ] }

#one-liner to convert deep open
deep_open = JSON.parse(hash.to_json, object_class: OpenStruct)

#now you can do this sorcery!
deep_open.some.map(&:very)
=> ["deep", "nested", "hash"]

lacostenycoder
  • 10,623
  • 4
  • 31
  • 48
0

If it's in Rspec stubs will work too.

let(:item) { stub(current: 1, total: 1) } 
Eddie
  • 1,428
  • 14
  • 24
0

If round-tripping JSON seems like too much overhead:

# Constructor like OpenStruct, but not open; and deep, not shallow
# Example: hash = { a: 1, b: { c: 2, d: 3 } }
#          obj = DeepStruct.new(hash)
#          obj.a # read
#          obj.b.c += 1 # read and write 
#          obj.x # error
class DeepStruct
  def initialize(hash)
    hash.each do |k, v|
      self.class.send(:attr_accessor, k)
      instance_variable_set("@#{k}", v.is_a?(Hash) ? DeepStruct.new(v) : v)
    end
  end
end
-1

Because Hash doesn't have a version method.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653