The Data Class in Ruby 3.2+
Data is Ruby’s built-in way to define simple classes for immutable value objects. Added in Ruby 3.2, it’s the pragmatic choice when you want something like a Struct but with immutability enforced by default — no setters, no accidental mutation after creation.
Defining a Data Class
Point = Data.define(:x, :y)
origin = Point.new(x: 0, y: 0)
origin.x # => 0
Both positional and keyword arguments work at construction time:
Point.new(1, 2) # positional — args are mapped to members in order
Point.new(x: 1, y: 2) # keyword — same result
Immutability
Data instances are frozen immediately on creation:
p = Point.new(x: 1, y: 2)
p.x = 3 # NoMethodError: undefined method `x=' for Point
There are no setter methods. This makes Data a good fit for value objects where identity is based on content, not mutability.
Mandatory Members
All members defined with Data.define are required — there’s no way to provide defaults:
Config = Data.define(:host, :port)
Config.new(host: "localhost")
# ArgumentError: missing keyword: :port
If you need defaults, Struct is a better fit, or provide your own initialize:
Config = Data.define(:host, :port) do
def initialize(host:, port: 3000)
super(host: host, port: port)
end
end
Accessing Members
Color = Data.define(:red, :green, :blue)
c = Color.new(red: 255, green: 128, blue: 64)
c.red # => 255
c.green # => 128
c.to_h # => {:red=>255, :green=>128, :blue=>64}
Color.members # => [:red, :green, :blue]
Equality and Hashing
Two Data instances with the same class and member values are equal, and they hash identically — making them natural Hash keys:
a = Point.new(x: 1, y: 2)
b = Point.new(x: 1, y: 2)
a == b # => true
a.hash == b.hash # => true — can use as Hash keys
h = { a => "origin point" }
h[b] # => "origin point"
Pattern Matching
Data pairs particularly well with Ruby’s pattern matching syntax. Each Data class implements deconstruct (returns an array) and deconstruct_keys (returns a hash):
Point = Data.define(:x, :y)
case Point.new(x: 10, y: 20)
in [Integer, Integer]
puts "Matched as array: #{$1}, #{$2}"
in x:, y:
puts "Matched as hash pattern: x=#{x}, y=#{y}"
end
This makes it easy to destructure Data objects in case/in expressions, especially when working with collections of value objects.
When Data Falls Short
Mutable members are not protected. If a member holds a reference to a mutable object, that object can still be changed:
Record = Data.define(:values)
r = Record.new(values: [1, 2, 3])
r.values << 4 # Allowed — the Array inside is mutable
Data freezes the container, not its contents. If you need deep immutability, freeze members manually before assignment.
Not Enumerable. Data does not include Enumerable or provide each. Use Struct if you need to iterate over members.
No inheritance hierarchy. Data.define creates a simple leaf class. For shared behavior across multiple Data classes, Struct or a regular class is more flexible.
Data vs Struct
| Data | Struct | |
|---|---|---|
| Frozen by default | Yes | No |
| Mandatory members | Yes | No |
| Setter methods | None | Yes |
| Pattern matching helpers | Yes | No |
| Enumerable support | No | Yes |
The rule of thumb: use Data for plain value objects where immutability is desirable, and Struct when you need mutability, defaults, or iteration.
See Also
- Struct Guide — Ruby’s more flexible alternative for grouped data
- Pattern Matching — using
case/inwith Ruby’s built-in types - Ruby Value Objects — patterns for modeling immutable data in Ruby