Ruby's New Data Class (Ruby 3.2+): The Feature You Didn't Know You Needed
Ruby 3.2, released in December 2022, introduced the Data class. A built-in, immutable value object for Rubyists who want simple, safe, and fast data structures without the boilerplate of custom classes or Structs. If you haven’t upgraded to Ruby 3.2 or later, you’re missing out on this powerful feature!
The Purpose of Ruby’s Data Class
The purpose of the Data class is to store immutable, atomic values. Think of it as a safer, simpler way to represent
data that shouldn’t change after creation. It’s ideal for representing simple data, configuration, or value objects.
Why Ruby Needed a Data Class: The Value Object Pattern
The motivation for Ruby’s Data class comes from the need for simple, immutable value objects, an idea popularized by Martin Fowler and widely used in many programming languages. Value objects are small objects whose equality is based on their values, not their identity. They are ideal for representing things like coordinates, money, or ranges, and are a core concept in Domain-Driven Design.
Before Data, Rubyists often used Struct for this purpose, but Structs are mutable and behave like collections, which can lead to subtle bugs and confusion. The Data class was introduced in Ruby 3.2 to provide a safer, clearer alternative, inspired by the value object pattern described by Fowler and others.
For more background, see:
Basic Example Usage
Person = Data.define(:name, :age)
p = Person.new("Julio", 30)
p.name # => "Julio"
p.age # => 30
p[:name] # => "Julio"
p.to_h # => { name: "Julio", age: 30 }
Optional block
You can pass an optional block to Data.define to add custom methods:
Point = Data.define(:x, :y) do
def distance
Math.sqrt(x**2 + y**2)
end
end
p = Point.new(3, 4)
p.distance # => 5.0
Instantiating Data Objects
You can create instances using .new, [], keyword, or positional arguments:
Person = Data.define(:name, :age)
Person.new("Julio", 30)
Person["Julio", 30]
Person.new(name: "Julio", age: 30)
Person[name: "Julio", age: 30]
Immutability and #frozen?
All Data objects are frozen by default:
p = Person.new("Julio", 30)
p.frozen? # => true
Comparison with Struct
Data objects have far fewer instance methods than Structs, reducing the risk of method conflicts and making them easier
to reason about. You get only the essentials: ==, [], to_h, etc.
For the complete list of available methods, see the official Ruby Data documentation .
Unlike Struct, which requires manual copying or mutation, Data provides a unique #with method for creating a copy with updated fields (without mutation):
p2 = p.with(age: 31)
p2.age # => 31
p.age # => 30
Before Ruby 3.2, most Rubyists used Struct for simple value objects. But Structs are mutable and can be accidentally changed, leading to hidden bugs.
Old Way: Using Struct
Person = Struct.new(:name, :age)
p = Person.new("Julio", 30)
p.name = "Ana" # Oops! This changes the name
p.age += 1 # Oops! This changes the age
Structs are convenient, but their mutability can cause problems when you expect your data to remain unchanged.
New Way: Using Data
Person = Data.define(:name, :age)
p = Person.new("Julio", 30)
p.name = "Ana" # => NoMethodError: undefined method `name=' for #<data Person>
p.age += 1 # => NoMethodError: undefined method `age=' for #<data Person>
With Data, your objects are truly immutable. You can’t accidentally change their fields, making your code safer and easier to reason about.
Migration Tips
- Replace simple Structs with
Datafor better safety. - Use
Datafor configuration, Data Transfer Objects (DTOs), or anywhere you want immutable value objects. Datais available in Ruby 3.2+, so check your Ruby version before migrating.
Real Life Scenarios Where Data Shines
Here are some practical situations where using Data instead of Struct makes a real difference:
API Response Object
ApiResponse = Data.define(:status, :body)
response = ApiResponse.new(200, "{...}")
# response.status and response.body can't be changed
Configuration Settings
Config = Data.define(:host, :port)
config = Config.new("localhost", 3000)
# config.host and config.port are immutable
Domain Event
UserRegistered = Data.define(:user_id, :timestamp)
event = UserRegistered.new(42, Time.now)
# event.user_id and event.timestamp are fixed
Background Job Arguments
JobArgs = Data.define(:user_id, :action)
args = JobArgs.new(7, "send_email")
# Safe to pass args to Sidekiq or other job processors
These scenarios show how Data helps you write safer, more reliable Ruby code in real-world projects.
Final Thoughts
Ruby’s Data class is a welcome addition for anyone who wants to write safer, cleaner, and more maintainable code. By making value objects truly immutable, it helps prevent subtle bugs and makes your intent clear. Whether you’re building APIs, handling configuration, or passing data between services, using Data can simplify your code and improve reliability.
If you haven’t tried it yet, consider refactoring some of your Structs or custom classes to use Data. You’ll likely find your code easier to test, reason about, and maintain. As Ruby continues to evolve, features like Data show a commitment to modern programming practices and developer happiness. Give it a try in your next project and see the difference for yourself!
If you don’t have the time to do it yourself you can hire our team to do it for you, We can help!
References
- Ruby 3.2 Release Notes: https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/
- Official Ruby Data Documentation (Ruby 3.4): https://docs.ruby-lang.org/en/3.4/Data.html
- Ruby Data Class Feature Request (Ruby Issue #16122): https://bugs.ruby-lang.org/issues/16122