CrackedRuby logo

CrackedRuby

OpenStruct

A comprehensive guide to Ruby's OpenStruct class for creating objects with dynamic attribute access patterns.

Standard Library Data Structures
4.1.4

Overview

OpenStruct provides a data structure that creates objects with arbitrary attributes accessible through method calls. Ruby implements OpenStruct as a wrapper around a hash, using method_missing to convert method calls into hash operations. The class creates objects where attributes can be added, modified, and accessed dynamically without predefined instance variables or accessor methods.

OpenStruct objects behave like regular Ruby objects for attribute access but maintain the flexibility of hash-like storage internally. When you call a method on an OpenStruct instance, Ruby first checks if the method exists as a defined method. If not, method_missing intercepts the call and either retrieves the corresponding hash value for getter methods or stores a new value for setter methods.

The primary use cases include configuration objects, test doubles, data transfer objects, and scenarios where the structure of data is not known at compile time. OpenStruct excels when you need object-like attribute access but want the flexibility to add attributes dynamically.

require 'ostruct'

person = OpenStruct.new
person.name = "Alice"
person.age = 30
person.city = "Portland"

puts person.name    # => "Alice"
puts person.age     # => 30
puts person.city    # => "Portland"

OpenStruct also accepts initialization with a hash, converting hash keys to method names:

config = OpenStruct.new(
  database: "postgres",
  host: "localhost",
  port: 5432,
  timeout: 30
)

puts config.database  # => "postgres"
puts config.host      # => "localhost"

The class inherits from BasicObject in some Ruby versions and Object in others, but consistently provides hash table functionality with object method syntax. OpenStruct converts string keys to symbols internally and raises NoMethodError for undefined attributes when accessed as getters.

Basic Usage

Creating OpenStruct instances requires loading the ostruct library, as it's part of Ruby's standard library but not loaded by default. Once loaded, you can create instances either empty or with initial data.

require 'ostruct'

# Empty OpenStruct
empty_struct = OpenStruct.new

# OpenStruct with initial data
user_data = OpenStruct.new(
  id: 12345,
  username: "developer",
  email: "dev@example.com",
  active: true
)

puts user_data.id        # => 12345
puts user_data.username  # => "developer"
puts user_data.active    # => true

Attribute assignment works through setter methods, which OpenStruct generates dynamically. You can add new attributes at any time after object creation:

user_data.last_login = Time.now
user_data.preferences = { theme: "dark", notifications: true }
user_data.role = "admin"

puts user_data.last_login    # => 2025-08-30 15:30:42 UTC
puts user_data.preferences   # => { theme: "dark", notifications: true }
puts user_data.role          # => "admin"

OpenStruct provides several methods for introspection and manipulation. The to_h method returns the underlying hash representation, while each_pair allows iteration over attribute-value pairs:

settings = OpenStruct.new(
  theme: "light",
  font_size: 14,
  auto_save: true
)

# Convert to hash
hash = settings.to_h
puts hash  # => { theme: "light", font_size: 14, auto_save: true }

# Iterate over attributes
settings.each_pair do |key, value|
  puts "#{key}: #{value}"
end
# Output:
# theme: light
# font_size: 14
# auto_save: true

The delete_field method removes attributes and their associated accessor methods:

user = OpenStruct.new(name: "Bob", age: 25, temp_data: "remove_this")

puts user.temp_data  # => "remove_this"

user.delete_field(:temp_data)
# user.temp_data  # => NoMethodError: undefined method `temp_data'

puts user.to_h  # => { name: "Bob", age: 25 }

OpenStruct supports method chaining and nested structures:

api_config = OpenStruct.new
api_config.endpoints = OpenStruct.new
api_config.endpoints.users = "/api/v1/users"
api_config.endpoints.posts = "/api/v1/posts"
api_config.rate_limits = OpenStruct.new(requests_per_minute: 60)

puts api_config.endpoints.users                    # => "/api/v1/users"
puts api_config.rate_limits.requests_per_minute    # => 60

Performance & Memory

OpenStruct carries significant performance overhead compared to regular Ruby objects, structs, or hash access. The dynamic method generation and method_missing mechanism creates measurable latency, particularly in performance-critical code paths.

Benchmark comparisons demonstrate the performance difference between OpenStruct and alternatives:

require 'ostruct'
require 'benchmark'

# Setup data
hash = { name: "John", age: 30, city: "Boston" }
ostruct = OpenStruct.new(hash)
regular_struct = Struct.new(:name, :age, :city).new("John", 30, "Boston")

n = 1_000_000

Benchmark.bmbm do |x|
  x.report("Hash access:") do
    n.times { hash[:name]; hash[:age]; hash[:city] }
  end
  
  x.report("OpenStruct:") do
    n.times { ostruct.name; ostruct.age; ostruct.city }
  end
  
  x.report("Struct:") do
    n.times { regular_struct.name; regular_struct.age; regular_struct.city }
  end
end

# Typical results show OpenStruct 3-5x slower than hash access
# and 2-3x slower than Struct access

Memory usage patterns differ significantly between OpenStruct and alternatives. OpenStruct maintains both the underlying hash and generates singleton methods for each attribute, increasing memory footprint:

require 'objspace'

# Memory comparison
hash = { name: "Alice", department: "Engineering", salary: 75000 }
ostruct = OpenStruct.new(hash)

hash_size = ObjectSpace.memsize_of(hash)
ostruct_size = ObjectSpace.memsize_of(ostruct)

puts "Hash memory: #{hash_size} bytes"
puts "OpenStruct memory: #{ostruct_size} bytes"
puts "Overhead: #{((ostruct_size.to_f / hash_size) - 1) * 100}%"

# OpenStruct typically uses 2-4x more memory than equivalent hash

Dynamic method creation affects garbage collection patterns. Each attribute access on a new OpenStruct potentially creates singleton methods, contributing to method cache pressure:

# Method creation impact
1000.times do |i|
  obj = OpenStruct.new
  obj.send("attribute_#{i}=", "value_#{i}")
  # Each assignment creates new singleton methods
end

# This pattern can impact GC performance in high-throughput scenarios

For scenarios with many small OpenStruct instances, consider object pooling or alternative data structures:

# Alternative: Struct for known attributes
UserData = Struct.new(:name, :email, :role, keyword_init: true)
user = UserData.new(name: "Bob", email: "bob@example.com", role: "user")

# Alternative: Hash with method delegation
class FlexibleConfig < Hash
  def method_missing(name, *args)
    if name.to_s.end_with?('=')
      self[name.to_s.chomp('=')] = args.first
    else
      self[name.to_s]
    end
  end
end

config = FlexibleConfig.new
config.database = "mysql"
puts config.database  # => "mysql"

Common Pitfalls

OpenStruct's dynamic nature creates several gotchas that can cause unexpected behavior in production applications. Understanding these pitfalls helps avoid debugging sessions and runtime errors.

Method naming conflicts represent a primary source of confusion. OpenStruct generates methods dynamically, but these methods can conflict with existing Object methods or Ruby keywords:

# Dangerous attribute names
problematic = OpenStruct.new
problematic.class = "User"          # Conflicts with Object#class
problematic.method = "POST"         # Conflicts with Object#method  
problematic.send = "email"          # Conflicts with Object#send

# These assignments may not work as expected:
puts problematic.class   # => OpenStruct, not "User"
puts problematic.method  # => Method object, not "POST"

# Safe alternative - use underscore prefix
safe = OpenStruct.new
safe._class = "User"
safe._method = "POST"
safe._send = "email"

puts safe._class   # => "User"
puts safe._method  # => "POST"

Hash key conversion creates inconsistent behavior between string and symbol keys. OpenStruct converts all keys to symbols internally, but this conversion doesn't always behave intuitively:

# Key conversion gotcha
mixed_keys = OpenStruct.new("name" => "John", :age => 30)

puts mixed_keys.name  # => "John"
puts mixed_keys.age   # => 30

# But the underlying hash shows symbol keys
puts mixed_keys.to_h  # => { :name => "John", :age => 30 }

# This affects key-based operations
original_hash = { "name" => "John", "age" => 30 }
ostruct = OpenStruct.new(original_hash)

# Original hash unchanged
puts original_hash.keys  # => ["name", "age"]
# But OpenStruct uses symbols
puts ostruct.to_h.keys   # => [:name, :age]

Nil attribute access raises NoMethodError rather than returning nil, unlike hash access:

user = OpenStruct.new(name: "Alice")

# This works fine
puts user.name  # => "Alice"

# This raises NoMethodError
begin
  puts user.nonexistent_attribute
rescue NoMethodError => e
  puts "Error: #{e.message}"
end
# => Error: undefined method `nonexistent_attribute' for #<OpenStruct>

# Compare with hash behavior
hash = { name: "Alice" }
puts hash[:nonexistent_key]  # => nil (no error)

Serialization and marshaling can lose OpenStruct's dynamic methods across process boundaries:

# Serialization gotcha
original = OpenStruct.new(name: "Bob", age: 25)
original.dynamic_method = "added later"

# Marshal and unmarshal
serialized = Marshal.dump(original)
restored = Marshal.load(serialized)

puts restored.name           # => "Bob" (works)
puts restored.dynamic_method # => "added later" (works)

# But method inspection differs
puts original.methods.include?(:name)           # => true
puts restored.methods.include?(:name)           # => true (methods recreated)

Equality comparisons can produce unexpected results when comparing OpenStruct instances with other object types:

# Equality pitfall
hash_data = { name: "Carol", role: "admin" }
ostruct1 = OpenStruct.new(hash_data)
ostruct2 = OpenStruct.new(hash_data)

puts ostruct1 == ostruct2        # => true (same content)
puts ostruct1 == hash_data       # => false (different types)
puts ostruct1.to_h == hash_data  # => false (symbol vs string keys)

# Type checking considerations
puts ostruct1.is_a?(Hash)        # => false
puts ostruct1.respond_to?(:each) # => true (but behaves differently than Hash#each)

Thread safety issues emerge when multiple threads modify the same OpenStruct instance:

# Thread safety pitfall
shared_config = OpenStruct.new(counter: 0)

threads = 10.times.map do
  Thread.new do
    100.times do
      current = shared_config.counter
      shared_config.counter = current + 1
      # Race condition: final count may be less than 1000
    end
  end
end

threads.each(&:join)
puts shared_config.counter  # => Likely less than 1000 due to race condition

Reference

Core Methods

Method Parameters Returns Description
OpenStruct.new(hash = nil) hash (Hash, optional) OpenStruct Creates new instance, converts hash keys to symbols
#[](name) name (Symbol/String) Object Retrieves attribute value by name
#[]=(name, value) name (Symbol/String), value (Object) Object Sets attribute value by name
#delete_field(name) name (Symbol) Object Removes attribute and returns its value
#dig(*keys) *keys (Array) Object Retrieves nested attribute values
`#each_pair { name, value block}` Block
#eql?(other) other (Object) Boolean Compares OpenStruct instances for equality
#hash None Integer Returns hash code for the instance
#inspect None String Returns string representation showing attributes
#marshal_dump None Hash Returns hash for serialization
#marshal_load(hash) hash (Hash) OpenStruct Restores instance from serialized hash
#to_h None Hash Returns hash representation of attributes

Attribute Access Methods

OpenStruct dynamically generates getter and setter methods for each attribute:

Pattern Parameters Returns Description
#attribute_name None Object Getter method for attribute
#attribute_name=(value) value (Object) Object Setter method for attribute
#attribute_name? None Boolean Predicate method, returns truthiness of attribute

Comparison and Conversion

Method Parameters Returns Description
#==(other) other (Object) Boolean Equality comparison with another OpenStruct
#to_s None String String representation of the instance

Error Conditions

Scenario Exception Description
Accessing undefined attribute NoMethodError Raised when calling getter for nonexistent attribute
Conflicting method names Various Unexpected behavior when attribute names conflict with Object methods
Invalid attribute names NoMethodError Raised for attribute names that cannot be converted to valid method names

Usage Patterns

Pattern Example Use Case
Configuration object config = OpenStruct.new(host: "localhost", port: 8080) Application settings with unknown structure
Data transfer object user = OpenStruct.new(json_data) Converting API responses to object attributes
Test double mock_service = OpenStruct.new(call: "success") Simple test stubs with method responses
Nested structures app.database.connection.timeout = 30 Hierarchical configuration management

Thread Safety Considerations

Access Type Thread Safety Recommendation
Reading existing attributes Generally safe No synchronization needed for read-only access
Writing to existing attributes Not thread-safe Requires external synchronization
Adding new attributes Not thread-safe Requires external synchronization
Method generation Not thread-safe Initialize all attributes before concurrent access

Performance Characteristics

Operation Relative Performance Notes
Attribute read 3-5x slower than Hash Due to method_missing overhead
Attribute write 4-6x slower than Hash Method generation + hash write
Object creation 2-3x slower than Struct Dynamic method setup overhead
Memory usage 2-4x more than Hash Stores hash + singleton methods