59

How can I convert a hash into a struct in ruby?

Given this:

h = { :a => 1, :b => 2 } 

I want a struct such that:

s.a == 1 s.b == 2 

12 Answers 12

77

If you already have a struct defined, and you want to instantiate an instance with a hash:

Person = Struct.new(:first_name, :last_name, :age) person_hash = { first_name: "Foo", last_name: "Bar", age: 29 } person = Person.new(*person_hash.values_at(*Person.members)) => #<struct Person first_name="Foo", last_name="Bar", age=29> 
Sign up to request clarification or add additional context in comments.

3 Comments

Thank you! I'm designing a gem that can be invoked from a command or from outer code, each supplying options (using OptionParser or a Hash respectively). This allows easy filtering of options during initialization of my gem. And the Struct helps to "self-document" allowed options as well!
Really nice solution
In ruby > 2.0 you can do this slightly more concisely with the double splat operator (**) - Person.new(**person_hash)
72

If it doesn't specifically have to be a Struct and instead can be an OpenStruct:

pry(main)> require 'ostruct' pry(main)> s = OpenStruct.new(h) => #<OpenStruct a=1, b=2> pry(main)> puts s.a, s.b 1 2 

8 Comments

Be aware that OpenStructs can be incredibly slow to use. Fine for a small number of small objects, but they scale badly. Some further info here: stackoverflow.com/questions/1177594/ruby-struct-vs-openstruct
@AFaderDarkly I think their speed issues are well documented, but thanks.
I believe the last command should read: pry(main)> puts s.a, s.b or line 2 should read pry(main)> o = OpenStruct.new(h)
@DaveNewton: Not on this page they haven't. Or on numerous sites that recommend their use. The original question asked for a struct - it is only polite - I think - to warn of the trade-off inherent in using a different solution.
fwiw I came to this page looking to replace OpenStruct with Struct because of performance issues. Please at least update your answer with a warning.
|
62

Since Hash key order is guaranteed in Ruby 1.9+:

Struct.new(*h.keys).new(*h.values) 

6 Comments

Good to know. I though I read that somewhere but didn't remember where. Thanks!
This doesn't appear to work (at least in Ruby 2.2.0): Struct.new(*h.keys) raises: NameError: identifier my_key needs to be constant
@Joe it does work fine. I think you used string keys for your hash, which is the cause for the error. The error is telling you that it needs a constant value, ie, a symbol rather than a string. I can repro the error in 2.1.5, goes away if I switch to symbol.
@Joe minor correction: the real reason for the NameError is because if you supplied a string as the first argument to Struct::new, it assumes that it's the name of the class it will create so it will attempt to convert that to a constant and fail if the string is all lower case (because constants have to be capitalized in Ruby). The fix is to either a) provide a capitalized string as a first argument (Struct.new('MyClass', *h.keys)) or use symbols for your hash's keys as ehsanul suggested.
Ran into the same issue as Joe, and had to convert my string arguments first: Struct.new(*h.keys.map(&:to_sym)).new(*h.values)
|
19

This is based on @elado's answer above, but using the keyword_init value (Struct Documentation)

You could simply do this:

Person = Struct.new(:first_name, :last_name, :age, keyword_init: true) person_hash = { first_name: "Foo", last_name: "Bar", age: 29 } person = Person.new(person_hash) => #<struct Person first_name="Foo", last_name="Bar", age=29> 

1 Comment

Note that keyword_init: true was only introduced in Ruby 2.5, whereas 2.4 is still within support.
11

The following creates a struct from a hash in a reliable way (since hash order is not guaranteed in ruby):

s = Struct.new(*(k = h.keys)).new(*h.values_at(*k)) 

Comments

8

Having Hash#to_struct is quite practical:

class Hash def to_struct Struct.new(*keys).new(*values) end end 

And some examples:

>> { a: 1, b: 2 }.to_struct => #<struct a=1, b=2> >> { a: 1, b: 2 }.to_struct.a => 1 >> { a: 1, b: 2 }.to_struct.b => 2 >> { a: 1, b: 2 }.to_struct.c NoMethodError: undefined method `c` for #<struct a=1, b=2> 

Deep to_struct that works with arrays:

class Array def to_struct map { |value| value.respond_to?(:to_struct) ? value.to_struct : value } end end class Hash def to_struct Struct.new(*keys).new(*values.to_struct) end end 

4 Comments

Its good, But if its json hash ['name'], needs to symbolize the keys.
@7urkm3n What does that even mean?
@konsolebox hash is key/value, if hash has json value then key name has to be symbolized. as i remember this. Sorry its 4yrs old thread :)
@7urkm3n Still don't get it. What is a "json hash" or a "json value". Care to give an example? As far as I know, JSON doesn't have symbols, only strings. So why the need to convert strings to symbols? JSON keys aren't symbols.
2

You can convert from Hash to Struct using the following code:

Struct.new(*my_hash.keys.map(&:to_sym)).new(*my_hash.values) 

Ensure you convert all keys to symbols, as it will error on String keys, NameError: identifier my_key needs to be constant

I personally recommend adding a monkey patch to the Hash class because this is such a powerful action

# config/initializers/core_extensions.rb Hash.class_eval do def to_struct Struct.new(*keys.map(&:to_sym)).new(*values) end end 

Comments

1

Here's an example to map the values to the proper order of the Struct:

require 'securerandom' Message = Struct.new(:to, :from, :message, :invitee) message_params = {from: "[email protected]", to: "[email protected]", invitee: SecureRandom.uuid, message: "hello"} if Message.members.sort == message_params.keys.sort # Do something with the return struct object here Message.new *Message.members.map {|k| message_params[k] } else raise "Invalid keys for Message" end 

Comments

1

This gives a clean plain read-only object, similar to a ruby Struct but with deep conversion and extra to_h method to get struct at any point as Hash.

Example

foo = {a:{b:{c:123}}}.to_struct foo.a.b.c # 123 foo.a.to_h # {b:{c:123}} 

Ruby code

class Hash def to_struct Class.new.tap do |c| c.define_singleton_method(:to_h) do m_list = methods(false) - [:to_h] m_list.inject({}) do |h, m| h[m] = send(m) h[m] = h[m].to_h if h[m].class == Class h end end each do |k, v| v = v.to_struct if v.class == Hash c.define_singleton_method(k) { v } end end end end 

Not exactly the answer to a question (not a ruby Struct object), but I needed just this while looking for an answer, so I will just post the answer here.

Comments

1

If you need a recursive version, here's a neat hack/solution

a_hash = {a: {b: {c: 'x'}}} structs_inside_structs = JSON.parse( a_hash.to_json, object_class: OpenStruct ) # => #<OpenStruct a=#<OpenStruct b=#<OpenStruct c="x">>> structs_inside_structs.a.b.c # => "x" 

Comments

0

TL;DR;

use OpenStruct.new(hash)

New Better way

hash = {"Name" => "Taimoor", "id" => "222", "SomeKey" => "Some value", "attributes" => {"type" => 'User', 'role' => 'manager'}} OpenStruct.new(hash) 

This will only convert 1st level of hash to struct. To convert nested attributes hash to struct I did this

hash.attributes = OpenStruct.new(hash.attributes) 

OLD Way

I had a hash with string keys

{"Name" => "Taimoor", "id" => "222", "SomeKey" => "Some value"} 

So I need to first convert keys to symbols hash.keys.map(&:to_sym) and to access those keys in the original hash, I used the hash.with_indifferent_access method on the hash.

def hash_to_struct(hash) Struct.new(*(k = hash.keys.map(&:to_sym))) .new(*hash.with_indifferent_access.values_at(*k)) end 

Now it will work for both symbol and string type keys of the hash.

Note: This will convert the hash to struct at one level only. For nested hash, you need to call this method on each level of nesting.

1 Comment

NOTE: OpenStruct incurs a performance penalty compared to Struct. It is now recommended to use Struct unless you really need the keys/attributes to be dynamic. See rubydoc.info/gems/rubocop/0.61.1/RuboCop/Cop/Performance/….
-1
require 'ds_hash' data = {a: {b: 123 }}.to_struct data.a.b == 123 # true data.a == {b: 123 } # true 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.