The Data Class in Ruby 3.2+

· 3 min read · Updated April 4, 2026 · intermediate
ruby-3.2 data-class immutable value-objects pattern-matching

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

DataStruct
Frozen by defaultYesNo
Mandatory membersYesNo
Setter methodsNoneYes
Pattern matching helpersYesNo
Enumerable supportNoYes

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