Checkpoint: OOP & Stdlib
Reviewed & published by Brayan K
This checkpoint ties together Ruby's object-oriented features and standard-library tools — inheritance, custom exceptions, dates, and serialization — into one realistic build challenge.
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.
Review the recap, attempt the challenge before peeking, then test yourself with the quiz.
What This Checkpoint Covers
- Inheritance, super, and method overriding
- Class methods, self, and constants
- Pattern matching with case/in
- Custom exception classes
- Dates, times, and formatting
- JSON/YAML serialization and Minitest
1️⃣ Recap: Inheritance, super & Pattern Matching
A subclass inherits its parent's behaviour and can override methods, calling super to reuse the parent's version. Constants live on the class, and case/in deconstructs data by shape.
# Recap: inheritance, super, class methods, constants, pattern matching.
class Animal
SPECIES_COUNT = 0 # a constant lives on the class
attr_reader :name
def initialize(name)
@name = name
end
def speak = "..."
def describe = "#{name} says #{speak}"
end
class Dog < Animal
def speak = "Woof" # override
def describe = "Dog: #{super}" # call parent's version
end
puts Dog.new("Rex").describe
# Pattern matching on a small data object:
result = { status: :ok, code: 200 }
case result
in { status: :ok, code: }
puts "OK with code #{code}"
in { status: :error }
puts "failed"
end2️⃣ Recap: Exceptions, Dates & JSON
Custom errors give failures a meaningful name, require 'date' brings calendar handling, and to_json serializes hashes for transport — the standard-library trio you'll reach for constantly.
# Recap: custom exceptions, date/time, JSON.
require 'json'
require 'date'
class OutOfStockError < StandardError; end
def order(qty, stock)
raise OutOfStockError, "only #{stock} left" if qty > stock
qty
end
begin
order(5, 2)
rescue OutOfStockError => e
puts "Order failed: #{e.message}"
end
event = { name: "Sale", on: Date.new(2026, 6, 18).to_s }
puts event.to_json3️⃣ Build Challenge: Shapes to JSON
Now combine it all: a class hierarchy with overridden area, a custom validation error, and JSON serialization of the whole collection. Try it yourself first — the solution is hidden below.
# 🎯 BUILD CHALLENGE: Shapes -> JSON
# Save as shapes.rb and run with: ruby shapes.rb
#
# 1. require 'json'.
# 2. Define InvalidShapeError < StandardError.
# 3. Build a Shape base class with: name, an area method that
# raises NotImplementedError, and a to_h that returns
# { name:, area: } with area rounded to 2 dp.
# 4. Subclass Circle(radius) — raise InvalidShapeError if radius <= 0,
# call super("circle"), area = PI * r**2.
# 5. Subclass Rectangle(w, h) — super("rectangle"), area = w * h.
# 6. Map an array of shapes through to_h and JSON.pretty_generate it.
# 7. Show the custom error firing when you create Circle.new(-1).
#
# ✅ Expected output:
# [
# {
# "name": "circle",
# "area": 12.57
# },
# {
# "name": "rectangle",
# "area": 12
# }
# ]
# Rejected: radius must be positive
# your code hereFull Solution
require 'json'
class InvalidShapeError < StandardError; end
class Shape
attr_reader :name
def initialize(name)
@name = name
end
def area
raise NotImplementedError, "subclass must define area"
end
def to_h
{ name: name, area: area.round(2) }
end
end
class Circle < Shape
def initialize(radius)
raise InvalidShapeError, "radius must be positive" if radius <= 0
super("circle")
@radius = radius
end
def area = Math::PI * @radius**2
end
class Rectangle < Shape
def initialize(w, h)
super("rectangle")
@w = w
@h = h
end
def area = @w * @h
end
shapes = [Circle.new(2), Rectangle.new(3, 4)]
# Serialize the whole hierarchy to JSON:
puts JSON.pretty_generate(shapes.map(&:to_h))
# Custom error in action:
begin
Circle.new(-1)
rescue InvalidShapeError => e
puts "Rejected: #{e.message}"
end
# ---- OUTPUT ----
# [
# { "name": "circle", "area": 12.57 },
# { "name": "rectangle", "area": 12 }
# ]
# Rejected: radius must be positive📝 Checkpoint Quiz
It calls the parent's method of the same name, forwarding the same arguments the current method received.
So a plain rescue catches it, while system-level Exception subclasses (like Ctrl-C) still propagate.
Give it a to_h (or to_json) that returns only primitives — strings, numbers, arrays, and hashes.
It matches by structure (deconstruction). With no matching branch and no else, it raises NoMatchingPatternError.
require 'date' for Date/DateTime; Time is built in and needs no require.
Three test methods ran, making four assertions total (one test made two assertions) — and all passed.
🎉 Checkpoint Complete!
- ✅ Inheritance, super, overriding, and constants
- ✅ Pattern matching with case/in
- ✅ Custom exceptions inheriting StandardError
- ✅ Dates/times and JSON/YAML serialization
- ✅ You built a shape hierarchy that serializes to JSON
Practice quiz
What does a bare super (no parentheses) do?
- Passes no arguments to the parent
- Calls a method named 'super'
- Forwards the same arguments the current method received to the parent's method
- Creates a new instance
Answer: Forwards the same arguments the current method received to the parent's method. Bare super forwards the exact arguments the current method received. super() passes none, and super(a, b) passes exactly those.
In the Circle subclass, what does super("circle") do?
- Passes just the string 'circle' to Shape#initialize
- Forwards radius to the parent
- Skips the parent constructor
- Raises an error
Answer: Passes just the string 'circle' to Shape#initialize. super("circle") deliberately passes one argument up to Shape#initialize, setting the name, regardless of Circle's own arguments.
Why raise NotImplementedError in the base Shape#area method?
- To make Shape run faster
- To return a default area of 0
- To skip the subclass
- To mark Shape as abstract so calling area on a raw Shape fails loudly
Answer: To mark Shape as abstract so calling area on a raw Shape fails loudly. It documents the contract 'every shape must define area' and fails clearly if a subclass forgets to override it.
Why should a custom exception inherit from StandardError rather than Exception?
- StandardError is faster
- A plain rescue catches StandardError, while Exception subclasses like Ctrl-C still propagate
- Exception cannot be subclassed
- StandardError prints a stack trace automatically
Answer: A plain rescue catches StandardError, while Exception subclasses like Ctrl-C still propagate. A bare rescue catches StandardError and its descendants but not system-level Exceptions like SystemExit or Interrupt.
How do you make a custom object JSON-serializable?
- Give it a to_h (or to_json) that returns only primitives
- Add a to_s method
- Inherit from JSON::Object
- Nothing; JSON handles any object
Answer: Give it a to_h (or to_json) that returns only primitives. JSON only understands strings, numbers, booleans, null, arrays, and hashes, so convert the object via to_h returning those primitives.
What does shapes.map(&:to_h) produce before JSON.pretty_generate?
- A single hash
- A JSON string
- A list of hashes, one per shape
- An array of strings
Answer: A list of hashes, one per shape. map(&:to_h) calls to_h on each shape, turning the array of objects into an array of hashes ready for JSON.
What does case/in (pattern matching) compare with?
- === case-equality
- Structure, by deconstructing the value
- Object identity
- String equality only
Answer: Structure, by deconstructing the value. case/in matches by structure (deconstruction), binding variables like { status: :ok, code: } from the shape of the data.
Which error fires when case/in has no matching branch and no else?
- NoMethodError
- ArgumentError
- StandardError
- NoMatchingPatternError
Answer: NoMatchingPatternError. With no matching in-clause and no else, case/in raises NoMatchingPatternError.
Which require do you need for Date, and what about Time?
- require 'date' for both
- require 'date' for Date/DateTime; Time is built in and needs no require
- require 'time' for both
- Neither needs a require
Answer: require 'date' for Date/DateTime; Time is built in and needs no require. Date and DateTime live in the date library (require 'date'), but Time is part of core Ruby.
A Minitest summary reads '3 runs, 4 assertions'. What does it tell you?
- 3 files were loaded
- Four tests failed
- Three test methods ran, making four assertions total
- Three assertions passed and one failed
Answer: Three test methods ran, making four assertions total. Runs counts test methods (3) and assertions counts total checks (4) — one test made two assertions.
Continue this course
- Previous: Testing with Minitest
- Next: Custom Iterators & Enumerator — Make your objects work with map/select by defining each and including Enumerable
- Quick reference: Ruby cheat sheet
Frequently asked questions
When does super pass arguments automatically?
A bare super (no parentheses) forwards the exact arguments the current method received to the parent's method of the same name. super() with empty parentheses passes no arguments, and super(a, b) passes exactly those. This distinction matters in initialize: super forwards everything, while super("circle") deliberately passes just one argument up to Shape#initialize.
Why raise NotImplementedError in the base class?
It turns the base Shape into an 'abstract' class: instances exist, but calling area on a raw Shape (or a subclass that forgot to define it) raises a clear error instead of silently misbehaving. This documents the contract — 'every shape must define area' — and fails loudly during development. Subclasses like Circle and Rectangle satisfy the contract by overriding area.
How does an object become JSON-serializable?
JSON only understands strings, numbers, booleans, null, arrays, and hashes. To serialize a custom object, give it a to_h (or to_json) that converts it into those primitives — here to_h returns { name:, area: }. Then JSON.generate or JSON.pretty_generate can handle it. Mapping a collection with map(&:to_h) turns a list of objects into a list of hashes ready for JSON.
Should a custom exception inherit from StandardError or Exception?
StandardError, almost always. A bare rescue catches StandardError and its descendants, but not the more critical Exception subclasses like SystemExit or Interrupt (Ctrl-C). Inheriting your InvalidShapeError from StandardError means ordinary rescue clauses catch it as expected, while leaving those system-level exceptions free to propagate and terminate the program when they should.