File I/O & Serialization
💾 File I/O & Serialization
📄 Reading Files
File.read loads an entire file into memory as one string — fine for small files, wasteful for huge ones. File.foreach streams line by line instead, similar in spirit to Python's for line in open(path) or PHP's fgets loop, without holding the whole file in memory at once.
✍️ Writing Files
File.open("output.txt", "w") without a block returns a file handle you have to remember to .close yourself — forget it, and the file may not be properly flushed to disk, especially if an exception interrupts your code before reaching the close call. The block form guarantees the file closes automatically once the block finishes, even if an exception is raised inside it — conceptually similar to Python's with open(...) as f: context manager, and safer than PHP's manual fopen/fclose pairing.
File modes work the same way across all three languages: "r" read (default), "w" write (truncates existing content), "a" append.
📦 JSON — the Universal Format
JSON is the natural choice for interoperating with literally anything else — an API, a JavaScript frontend, another language entirely. Ruby's require "json" plus .to_json/JSON.parse is directly comparable to PHP's json_encode/json_decode or Python's json.dumps/json.loads.
Recall Chapter 2 of Course 1: symbols and strings are genuinely different objects. JSON.parse defaults to string keys, since symbols aren't a real concept in JSON — pass symbolize_names: true explicitly if you want symbol keys back out, and remember any symbol values used going in also come back out as strings.
📜 YAML — Human-Readable Configuration
YAML is Ruby's own configuration-file format of choice — Rails' database.yml and Ruby's own Gemfile.lock-adjacent tooling lean on it heavily. Unlike JSON, YAML actually preserves Ruby-specific types like symbols across a round trip, at the cost of being Ruby-specific rather than a universal cross-language standard.
🗃️ Marshal — Ruby-Only Binary Serialization
Marshal can serialize almost any Ruby object exactly, including custom class instances — a genuine convenience JSON and YAML can't match without extra work. But Marshal.load executes as it reconstructs objects, and loading a maliciously crafted Marshal payload from an untrusted source (a network request, user upload, etc.) is a known way to execute arbitrary code. Keep Marshal for trusted, internal use — caching, temporary files your own program wrote — never for data that came from outside your control.
📜 Serialization Formats, Side by Side
Choosing a Format
| Format | Cross-language? | Preserves symbols/custom classes? | Safe for untrusted input? |
|---|---|---|---|
| JSON | Yes — universal | No — strings only | Yes |
| YAML | Mostly (other languages have YAML libraries) | Yes, within Ruby | Only with YAML.safe_load |
| Marshal | No — Ruby only | Yes, exactly | No — never on untrusted data |
💻 Coding Challenges
Challenge 1: Write and Read a File
Use the block form of File.open to write three lines of your choosing to diary.txt. Then use File.foreach to read the file back and print each line prefixed with its line number (starting at 1).
Goal: Practice the safe, block-based pattern for both writing and reading a file.
Challenge 2: JSON Round Trip
Build a hash representing a small user profile with symbol keys (name, email, and an array of tags). Convert it to a JSON string with .to_json, then parse it back with JSON.parse(json_string, symbolize_names: true). Confirm the parsed hash's keys are symbols again by calling .class on one of them.
Goal: See the string-vs-symbol round-trip issue firsthand and how symbolize_names fixes it.
Challenge 3: YAML vs JSON on the Same Data
Take the same hash from Challenge 2 and serialize it both with .to_yaml and .to_json. Print both strings side by side, then load each one back and compare whether the resulting hash's keys are symbols or strings in each case.
Goal: Directly observe the "preserves symbols" difference between the two formats from the comparison table.
When in doubt, reach for JSON: it's the safest for untrusted input, the most portable across languages and tools, and the format anything you build is most likely to eventually need to talk to. Save YAML for Ruby-specific configuration files where human readability matters, and treat Marshal as a specialized tool for trusted, same-process or same-machine caching — not a general-purpose serialization default.
🎯 What's Next
Next chapter: Testing with RSpec — describe/it blocks, expectations, and mocks/stubs, using the block-heavy syntax this course has been building toward since Course 1's final chapter.