class OpenStruct

An OpenStruct is a data structure, similar to a Hash, that allows the definition of arbitrary attributes with their accompanying values. This is accomplished by using Ruby’s metaprogramming to define methods on the class itself.

Examples ↑

require "ostruct"

person = OpenStruct.new
person.name = "John Smith"
person.age  = 70

person.name      # => "John Smith"
person.age       # => 70
person.address   # => nil

An OpenStruct employs a Hash internally to store the attributes and values and can even be initialized with one:

australia = OpenStruct.new(:country => "Australia", :capital => "Canberra")
  # => #<OpenStruct country="Australia", capital="Canberra">

Hash keys with spaces or characters that could normally not be used for method calls (e.g. ()[]*) will not be immediately available on the OpenStruct object as a method for retrieval or assignment, but can still be reached through the Object#send method or using [].

measurements = OpenStruct.new("length (in inches)" => 24)
measurements[:"length (in inches)"]       # => 24
measurements.send("length (in inches)")   # => 24

message = OpenStruct.new(:queued? => true)
message.queued?                           # => true
message.send("queued?=", false)
message.queued?                           # => false

Removing the presence of an attribute requires the execution of the delete_field method as setting the property value to nil will not remove the attribute.

first_pet  = OpenStruct.new(:name => "Rowdy", :owner => "John Smith")
second_pet = OpenStruct.new(:name => "Rowdy")

first_pet.owner = nil
first_pet                 # => #<OpenStruct name="Rowdy", owner=nil>
first_pet == second_pet   # => false

first_pet.delete_field(:owner)
first_pet                 # => #<OpenStruct name="Rowdy">
first_pet == second_pet   # => true

Ractor compatibility: A frozen OpenStruct with shareable values is itself shareable.

Caveats ↑

An OpenStruct utilizes Ruby’s method lookup structure to find and define the necessary methods for properties. This is accomplished through the methods method_missing and define_singleton_method.

This should be a consideration if there is a concern about the performance of the objects that are created, as there is much more overhead in the setting of these properties compared to using a Hash or a Struct. Creating an open struct from a small Hash and accessing a few of the entries can be 200 times slower than accessing the hash directly.

This is a potential security issue; building OpenStruct from untrusted user data (e.g. JSON web request) may be susceptible to a “symbol denial of service” attack since the keys create methods and names of methods are never garbage collected.

This may also be the source of incompatibilities between Ruby versions:

o = OpenStruct.new
o.then # => nil in Ruby < 2.6, enumerator for Ruby >= 2.6

Builtin methods may be overwritten this way, which may be a source of bugs or security issues:

o = OpenStruct.new
o.methods # => [:to_h, :marshal_load, :marshal_dump, :each_pair, ...
o.methods = [:foo, :bar]
o.methods # => [:foo, :bar]

To help remedy clashes, OpenStruct uses only protected/private methods ending with ! and defines aliases for builtin public methods by adding a !:

o = OpenStruct.new(make: 'Bentley', class: :luxury)
o.class # => :luxury
o.class! # => OpenStruct

It is recommended (but not enforced) to not use fields ending in !; Note that a subclass’ methods may not be overwritten, nor can OpenStruct’s own methods ending with !.

For all these reasons, consider not using OpenStruct at all.