Struct — Lightweight Data Objects in Ruby
Ruby’s Struct class gives you a quick way to bundle related data together without writing a full class. Think of it as a class with predetermined attributes — perfect for data Transfer Objects, configuration objects, or any place where you need a simple container for a few related values.
Creating a Struct
The simplest way to create a struct is with Struct.new, passing in attribute names as symbols or strings:
Point = Struct.new(:x, :y)
point = Point.new(10, 20)
point.x # => 10
point.y # => 20
You can also create an anonymous struct and use it directly:
person = Struct.new(:name, :age).new("Alice", 30)
person.name # => "Alice"
Struct as a Class
When you assign a struct to a constant (like Point), it becomes a reusable class:
Rectangle = Struct.new(:width, :height)
rect1 = Rectangle.new(100, 50)
rect2 = Rectangle.new(200, 100)
rect1.width # => 100
You can call methods on struct instances just like regular Ruby objects:
point = Point.new(3, 4)
point.to_h # => {:x=>3, :y=>4}
point.values # => [3, 4]
Keyword Arguments
By default, structs use positional arguments. You can switch to keyword arguments with keyword_init: true:
Config = Struct.new(:host, :port, :ssl, keyword_init: true)
config = Config.new(host: "localhost", port: 443, ssl: true)
config.host # => "localhost"
config.port # => 443
This makes calls more explicit and less error-prone when you have many attributes:
# Positional — easy to mix up order
User.new("john", 25, "john@example.com")
# Keyword — self-documenting
User.new(name: "john", age: 25, email: "john@example.com")
Adding Methods to Structs
Structs support custom methods right in the definition:
Product = Struct.new(:name, :price) do
def formatted_price
"$%.2f" % price
end
def discounted_price(discount)
price * (1 - discount)
end
end
product = Product.new("Widget", 29.99)
product.formatted_price # => "$29.99"
product.discounted_price(0.1) # => 26.991
This makes structs incredibly flexible — they’re lightweight classes that still support full Ruby object behavior.
Default Values
You can provide default values for attributes:
Server = Struct.new(:host, :port, :protocol) do
def initialize(host = "localhost", port = 80, protocol = "http")
super
end
end
Server.new # => #<struct Server host="localhost", port=80, protocol="http">
Or use a block to define defaults more elegantly:
Options = Struct.new(:debug, :verbose) do
def initialize(*)
super
self.debug ||= false
self.verbose ||= false
end
end
Ruby 3.2+ also supports default and default_proc for hash-like defaults.
Struct vs OpenStruct
Ruby provides two similar mechanisms for quick data objects:
| Feature | Struct | OpenStruct |
|---|---|---|
| Performance | Faster (fixed layout) | Slower (hash-backed) |
| Attributes | Defined at creation | Addable at runtime |
| Keyword args | Supported | Supported (Ruby 3.0+) |
| Memory | More efficient | More memory |
OpenStruct is slower because it uses a hash internally. Use Struct when you know the attributes ahead of time:
# Struct — faster, fixed schema
Point = Struct.new(:x, :y)
# OpenStruct — flexible, slower
point = OpenStruct.new(x: 1, y: 2)
point.z = 3 # can add attributes freely
Practical Examples
Configuration Object
AppConfig = Struct.new(:env, :debug, :database_url, keyword_init: true)
config = AppConfig.new(
env: "production",
debug: false,
database_url: "postgresql://localhost/app"
)
Data Transfer Object
UserDTO = Struct.new(:id, :name, :email, :created_at, keyword_init: true)
def build_user(data)
UserDTO.new(
id: data["id"],
name: data["name"],
email: data["email"],
created_at: Time.parse(data["created_at"])
)
end
Multiple Instances
Card = Struct.new(:rank, :suit)
deck = Card.new("A", "♠️")
deck.rank # => "A"
deck.suit # => "♠️"
Enumerating Struct Members
You can iterate over struct members programmatically:
Config = Struct.new(:host, :port, :ssl)
config = Config.new("example.com", 443, true)
config.members # => [:host, :port, :ssl]
config.each_pair do |name, value|
puts "#{name}: #{value}"
end
# host: example.com
# port: 443
# ssl: true
When to Use Structs
Structs shine in these scenarios:
- Configuration objects — group related settings
- Return values — multiple values from a method
- Data transfer — simple DTOs between layers
- Immutable data — structs work well with frozen objects
Avoid structs when you need complex validation, inheritance, or behavior-heavy objects. In those cases, write a full class.
See Also
- Working with Hashes in Ruby — Learn how hashes pair with structs for flexible data handling
- Working with Arrays in Ruby — Storing and manipulating collections of struct instances
- Ruby Blocks, Procs, and Lambdas — Advanced Ruby techniques that work well with structs