Struct & OpenStruct
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.
Learn Struct & OpenStruct in our free Ruby course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with examples you run in your browser, 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.
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 .
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.
Your turn. Fill in each ___ blank, then run it.
Model cart items as a Struct with a subtotal method, then sum them with sum(&:subtotal) . Run with ruby cart.rb .
📋 Quick Reference — Struct & Friends
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.