Reading and writing files
This chapter introduces reading and writing files, then serialization: turning Ruby objects into data that can be saved and reloaded later.
Principle
In a program, all data disappears when it terminates. To keep it, you must write it to a file on disk. On the next launch, you read it back.
Ruby provides the File class for all file operations. To save complex Ruby objects (not just text), you use serialization: Marshal (binary format) or JSON (human-readable text format).
Reading a file
The simplest way to read an entire file:
content = File.read('pokedex.txt')
puts content
File.readreads the entire file at once and returns a String.- If the file does not exist, Ruby raises an
Errno::ENOENTerror.
To read line by line (useful for large files):
lines = File.readlines('pokedex.txt')
lines.each { |line| puts line }
File.readlinesreturns an Array of Strings, one per line.
Writing to a file
# Write (overwrites existing content)
File.write('pokedex.txt', "Pikachu,25,electric\nCharizard,36,fire\n")
# Append to the end (without overwriting)
File.open('pokedex.txt', 'a') do |file|
file.puts 'Blastoise,40,water'
end
File.writecreates the file if it does not exist and overwrites all existing content.File.open('file', 'a')opens in append mode (afor append): new content is added at the end.
File.open with a block
File.open with a block is the safest approach: Ruby automatically closes the file at the end of the block, even if an error occurs:
File.open('pokedex.txt', 'r') do |file|
file.each_line do |line|
puts line
end
end
The main modes:
| Mode | Meaning |
|---|---|
'r' | Read-only (default). Error if the file doesn't exist |
'w' | Write. Creates the file or overwrites content |
'a' | Append. Creates the file or adds to the end |
'r+' | Read and write |
- Always prefer
File.openwith a block overFile.openwithout one. Without a block, you must remember to close the file manually.
Checking if a file exists
if File.exist?('pokedex.txt')
puts 'The file exists'
else
puts 'The file does not exist'
end
puts File.directory?('pokedex') # => true if it is a directory
puts File.size('pokedex.txt') # => size in bytes
- Always check
File.exist?before reading to avoid an error.
Marshal — binary serialization
Marshal converts any Ruby object into binary data, and vice versa. It is the simplest way to save complex objects:
# Save an object
pokemon = { name: 'Pikachu', level: 25, types: [:electric] }
File.open('pikachu.dat', 'wb') do |file|
Marshal.dump(pokemon, file)
end
# Load the object
loaded = File.open('pikachu.dat', 'rb') do |file|
Marshal.load(file)
end
p loaded # => {:name=>"Pikachu", :level=>25, :types=>[:electric]}
Marshal.dump(object, file)writes the object to the file.Marshal.load(file)reconstructs the object exactly as it was: classes, symbols, nested structures — everything is preserved.'wb'and'rb': binary modes are mandatory for Marshal. Without theb, encoding can corrupt the data.
Marshal also works with custom classes:
class Pokemon
attr_reader :name, :level
def initialize(name, level)
@name = name
@level = level
end
end
pikachu = Pokemon.new('Pikachu', 25)
# Save
File.open('pikachu.dat', 'wb') { |file| Marshal.dump(pikachu, file) }
# Load: we get a Pokemon object directly
loaded = File.open('pikachu.dat', 'rb') { |file| Marshal.load(file) }
puts loaded.name # => Pikachu
puts loaded.level # => 25
- After
Marshal.load, you get back aPokemoninstance directly, not a Hash to convert. This is Marshal's biggest strength.
JSON — text serialization
JSON is a human-readable text format that other languages can also use. You need to load the json library:
require 'json'
# Convert a Hash to JSON
pokemon_data = { name: 'Charizard', level: 50, types: ['fire', 'flying'] }
json_string = JSON.generate(pokemon_data)
puts json_string
# => {"name":"Charizard","level":50,"types":["fire","flying"]}
# Convert the JSON back to a Hash
parsed = JSON.parse(json_string)
puts parsed['name'] # => Charizard
JSON.generateconverts a Hash to a JSON String.JSON.parsedoes the reverse: it returns a Hash.- Warning: keys become Strings after parsing.
parsed[:name]returnsnil, you needparsed['name']. - JSON does not support Symbols:
:electricis converted to"electric".
To save and load from a file:
require 'json'
# Save
File.write('pokedex.json', JSON.generate(pokemon_data))
# Load
loaded_data = JSON.parse(File.read('pokedex.json'))
Marshal vs JSON
| Marshal | JSON | |
|---|---|---|
| Format | Binary (unreadable) | Text (readable) |
| Ruby types | Preserved (Symbol, classes) | Lost (everything becomes String/Array/Hash) |
| Portability | Ruby only | All languages |
| Reconstruction | Automatic | Manual (you must rebuild the objects) |
| Security | Never load untrusted data | Safe |
- Marshal is perfect for internal saves within a Ruby program.
- JSON is perfect for exchanging data with other programs or for readable configuration files.
Converting objects for JSON
Since JSON does not know about Ruby classes, you need to write conversion methods:
require 'json'
class Pokemon
attr_reader :name, :level, :types
def initialize(name, level, types)
@name = name
@level = level
@types = types
end
# Convert to Hash for JSON
def to_hash
return { name: @name, level: @level, types: @types.map { |type| type.to_s } }
end
# Reconstruct from a JSON Hash
def self.from_hash(data)
types = data['types'].map { |type| type.to_sym }
return Pokemon.new(data['name'], data['level'], types)
end
end
# Save as JSON
pikachu = Pokemon.new('Pikachu', 25, [:electric])
File.write('pikachu.json', JSON.generate(pikachu.to_hash))
# Load from JSON
data = JSON.parse(File.read('pikachu.json'))
loaded = Pokemon.from_hash(data)
puts loaded.name # => Pikachu
p loaded.types # => [:electric]
to_hashconverts Symbols to Strings (JSON does not support them).self.from_hashconverts Strings back to Symbols with.to_sym.- This is the "round-trip": object -> hash -> JSON -> hash -> object.
Conclusion
File.readandFile.writeare the simplest shortcuts.File.readlinesreturns an Array of lines.File.openwith a block automatically closes the file. Always prefer this form.- The modes
'r','w','a'control the type of access.'wb'/'rb'for binary mode. File.exist?checks existence before reading.- Marshal serializes to binary: preserves Ruby types, automatic reconstruction. Modes
'wb'/'rb'are mandatory. - JSON serializes to text: readable and portable, but loses Ruby types. You need
to_hashandself.from_hashfor the round-trip. - Marshal for internal saves. JSON for exchange and configuration.