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 |