Struct & OpenStruct

Reviewed & published by Brayan K

A Struct is a built-in Ruby shortcut that generates a small class bundling a fixed set of named attributes — complete with constructor, accessors, and value equality — in a single line of code.

Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

You'll also meet the flexible runtime-attribute OpenStruct and the immutable Data.define (Ruby 3.2+), and learn when each beats a full class.

What You'll Learn in This Lesson

1️⃣ Defining a Struct

Struct.new(:a, :b) returns a new class with readers and writers for each attribute and a constructor. Pass a block to add your own methods — inside them, you can refer to the attributes by name directly.

# Struct.new builds a small class with attributes in one line
Point = Struct.new(:x, :y)

p1 = Point.new(3, 4)
puts p1.x                 # 3
puts p1.y                 # 4
p1.x = 10                 # attributes are read/write
puts p1.x                 # 10

# You can add methods in a block:
Point2 = Struct.new(:x, :y) do
  def distance_to_origin
    Math.sqrt(x**2 + y**2)
  end
end
puts Point2.new(3, 4).distance_to_origin   # 5.0

2️⃣ keyword_init, Equality & Conversions

Add keyword_init: true to construct by name instead of position. Structs come with value-based == for free, plus handy conversions like to_a, to_h, and the class method members.

# keyword_init lets you build with named arguments (clearer at the call site)
User = Struct.new(:name, :role, keyword_init: true)
u = User.new(name: "Ada", role: "admin")
puts u.name
puts u.role

# Structs get == for free: equal when all members match
P = Struct.new(:n)
puts (P.new(1) == P.new(1))    # true  (same struct class + members)
puts (P.new(1) == P.new(2))    # false

# Structs are also enumerable-ish: to_a, to_h, members
Coord = Struct.new(:lat, :lng)
c = Coord.new(51.5, -0.1)
p c.to_a
p c.to_h
p Coord.members

3️⃣ OpenStruct & Data.define

After require 'ostruct', an OpenStruct lets you add attributes at runtime and returns nil for unknown ones. For an immutable value object, Data.define (Ruby 3.2+) gives readers and value equality but freezes instances — no writers.

# OpenStruct: an object whose attributes you can set on the fly
require 'ostruct'
config = OpenStruct.new(host: "localhost", port: 3000)
puts config.host
config.timeout = 30          # add a brand-new attribute at runtime
puts config.timeout
puts config.missing.inspect  # unknown attribute -> nil (no error)

# Data.define (Ruby 3.2+): an IMMUTABLE value object
Measure = Data.define(:amount, :unit)
m = Measure.new(amount: 5, unit: "kg")
puts m.amount
puts m.unit
begin
  m.amount = 10              # frozen! cannot reassign
rescue NoMethodError => e
  puts "Immutable: #{e.message.split(' for ').first}"
end

Your turn. Fill in each ___ blank, then run it.

# 🎯 YOUR TURN — fill in the blanks, then run with: ruby book.rb

Book = Struct.new(:title, :author, :pages, ___: true) do  # 👉 build by NAME
  def summary
    "#{title} by #{author} (#{pages} pages)"
  end
end

b = Book.___(title: "Dune", author: "Herbert", pages: 412)  # 👉 make an instance
puts b.summary
puts b.pages

# ✅ Expected output:
#    Dune by Herbert (412 pages)
#    412

Mini-Challenge: A Shopping Cart

Model cart items as a Struct with a subtotal method, then sum them with sum(&:subtotal). Run with ruby cart.rb.

# 🎯 MINI-CHALLENGE: a shopping cart with a Struct.
# Save as cart.rb and run with: ruby cart.rb
#
# 1. Define Product = Struct.new(:name, :price, :qty, keyword_init: true)
#    with a method subtotal that returns price * qty.
# 2. Build a cart array of 3 products.
# 3. Print "<name>: $<subtotal>" for each, then the grand total
#    using cart.sum(&:subtotal).
#
# ✅ Example output:
#    Pen: $6
#    Book: $12
#    Mug: $16
#    Total: $34

# your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Struct & Friends

GoalRuby Syntax
Define a StructStruct.new(:x, :y)
Named argumentskeyword_init: true
Add methodsStruct.new(:x) do ... end
Convertto_a , to_h , members
Runtime attributesOpenStruct.new(...)
Immutable valueData.define(:a, :b)

🎉 Lesson Complete!

Practice quiz

What does Struct.new(:x, :y) return?

  • An instance with x and y
  • A hash
  • A new class with readers, writers, and a constructor
  • An array

Answer: A new class with readers, writers, and a constructor. Struct.new returns a new class that bundles the named attributes with accessors and a constructor.

By convention, you assign the result of Struct.new to a...

  • constant (capitalized name)
  • lowercase local variable
  • global variable
  • symbol

Answer: constant (capitalized name). Assign it to a constant like Point so it becomes a proper, named class.

What does keyword_init: true change about a Struct?

  • Makes it immutable
  • Removes the writers
  • Adds inheritance
  • Lets you construct with named arguments

Answer: Lets you construct with named arguments. With keyword_init: true you pass named arguments like User.new(name: "Ada", role: "admin").

Two Struct instances with the same members compare with == as...

  • always false
  • equal (true) when same struct class and members match
  • an error
  • equal only if same object_id

Answer: equal (true) when same struct class and members match. Structs get value-based == for free: equal when the same struct class and all members match.

Which method lists a Struct's attribute names?

  • members
  • keys
  • fields
  • attributes

Answer: members. The class method members returns the attribute names, e.g. [:lat, :lng].

Are a plain Struct's attributes read-only?

  • Yes, always frozen
  • Only with keyword_init
  • No, they have writers and can be reassigned
  • Only the first one

Answer: No, they have writers and can be reassigned. A plain Struct gives read/write accessors, so p1.x = 10 works.

What must you do before using OpenStruct?

  • Nothing, it's built in
  • require 'ostruct'
  • require 'set'
  • include OpenStruct

Answer: require 'ostruct'. OpenStruct isn't loaded by default — add require 'ostruct' first.

What does an OpenStruct return for an attribute you never set?

  • An error
  • false
  • an empty string
  • nil

Answer: nil. OpenStruct returns nil for unknown attributes, which is why typos can hide silently.

What does Data.define (Ruby 3.2+) create?

  • A mutable struct
  • An immutable (frozen) value object
  • A hash subclass
  • A module

Answer: An immutable (frozen) value object. Data.define creates an immutable value object — instances are frozen with no attribute writers.

How do you 'change' a field on a Data.define instance?

  • Reassign it directly
  • Use a setter
  • Call obj.with(field: newvalue) for a fresh copy
  • It can't change at all

Answer: Call obj.with(field: newvalue) for a fresh copy. Data instances are frozen, so .with(field: value) returns a new copy with the change.

Continue this course

Frequently asked questions

When should I use a Struct instead of a full class?

Reach for a Struct when you mainly need a bundle of named attributes with a little behaviour attached. In one line, Struct.new(:x, :y) gives you a constructor, reader and writer methods, value-based ==, and helpers like to_a, to_h, and members — boilerplate you'd otherwise write by hand. Graduate to a full class when the object grows real complexity: many private helpers, careful validation in the initializer, inheritance, or when you don't want the free attribute writers and equality that a Struct hands out.

What does keyword_init: true change?

Without it, you construct a Struct positionally: User.new("Ada", "admin") — concise but easy to mix up when there are several fields. With keyword_init: true you must pass named arguments: User.new(name: "Ada", role: "admin"), which is self-documenting and order-independent. As of Ruby 3.2 a Struct can even accept both styles. For anything beyond two or three fields, keyword initialization makes call sites far more readable.

How is OpenStruct different from a Struct, and why is it discouraged?

A Struct fixes its attributes up front, so its members are known and it's fast. An OpenStruct (after require 'ostruct') lets you set arbitrary attributes at runtime and returns nil for any you never defined — flexible, but slower, and it silently hides typos because a misspelled getter just returns nil instead of raising. OpenStruct is handy for quick prototypes, mock objects, or wrapping loose data like parsed JSON, but for production value objects a Struct or Data.define is safer and faster.

What is Data.define and how does it compare to Struct?

Data.define, introduced in Ruby 3.2, creates an immutable value object: like a Struct it gives you named readers, a keyword (and positional) constructor, value equality, and to_h, but it has NO attribute writers, so instances are frozen after creation. To 'change' one you call .with(field: newvalue), which returns a fresh copy. Prefer Data.define when you want the safety and clarity of immutable values, and a mutable Struct only when you genuinely need to reassign fields in place.