Marshalling & Serialization
Master Ruby's marshalling and serialization capabilities with Marshal.dump/load and alternatives. Learn to serialize Ruby objects for storage, transmission, and caching while understanding critical security considerations and performance implications.
Understanding Marshalling & Serialization
Marshalling/Serialization: The process of converting Ruby objects into a format that can be stored, transmitted, or cached, and later reconstructed back into objects. It preserves object state and structure across different contexts.
Ruby's Marshal module provides binary serialization that maintains object types, instance variables, and relationships exactly as they were.
Use Cases
- Object caching
- Session storage
- Inter-process communication
- Persistence/state saving
- Deep object copying
Security Risks
- Code execution vulnerabilities
- Untrusted data deserialization
- Object injection attacks
- Memory exhaustion
- Version compatibility issues
Critical Security Warning
⚠️ Never use Marshal.load on untrusted data! It can execute arbitrary code and compromise your system. Always validate data sources and consider safer alternatives for external data.
Marshal.dump and Marshal.load
Basic Marshalling Operations
# Basic object marshalling
data = { name: "Alice", age: 30, skills: ["Ruby", "Rails"] }
# Serialize to binary string
marshalled = Marshal.dump(data)
puts marshalled.inspect # Binary data representation
# Deserialize back to object
restored_data = Marshal.load(marshalled)
puts restored_data # {:name=>"Alice", :age=>30, :skills=>["Ruby", "Rails"]}
# Marshal to file
File.open("data.marshal", "wb") do |file|
Marshal.dump(data, file)
end
# Load from file
restored_from_file = File.open("data.marshal", "rb") do |file|
Marshal.load(file)
end
# Complex objects with custom classes
class Person
attr_accessor :name, :age, :email
def initialize(name, age, email)
@name = name
@age = age
@email = email
end
def to_s
"#{@name} (#{@age}) - #{@email}"
end
end
person = Person.new("Bob", 25, "bob@example.com")
marshalled_person = Marshal.dump(person)
restored_person = Marshal.load(marshalled_person)
puts restored_person.name # "Bob"
puts restored_person.age # 25
puts restored_person # Bob (25) - bob@example.com
What Can Be Marshalled
# Basic data types
numbers = [1, 2.5, Rational(3, 4), Complex(1, 2)]
strings = ["hello", "world", "with\nnewlines"]
symbols = [:symbol, :"symbol with spaces"]
booleans = [true, false, nil]
marshalled_basic = Marshal.dump([numbers, strings, symbols, booleans])
restored_basic = Marshal.load(marshalled_basic)
# Collections
hash = { name: "Alice", scores: [95, 87, 92], active: true }
array = [1, "two", :three, { four: 4 }]
nested = { users: [{ name: "Alice" }, { name: "Bob" }] }
# Date and Time objects
require 'date'
time_data = {
now: Time.now,
date: Date.today,
datetime: DateTime.now
}
marshalled_time = Marshal.dump(time_data)
restored_time = Marshal.load(marshalled_time)
puts "Original time: #{time_data[:now]}"
puts "Restored time: #{restored_time[:now]}"
# Custom classes with instance variables
class Product
def initialize(name, price, tags)
@name = name
@price = price
@tags = tags
@created_at = Time.now
end
attr_reader :name, :price, :tags, :created_at
end
product = Product.new("Laptop", 999.99, ["electronics", "computer"])
marshalled_product = Marshal.dump(product)
restored_product = Marshal.load(marshalled_product)
puts "Product: #{restored_product.name} - $#{restored_product.price}"
puts "Tags: #{restored_product.tags.join(', ')}"
What Cannot Be Marshalled
- Anonymous classes/modules: Objects without constant names
- Singleton methods: Methods defined on specific instances
- File objects: IO streams and file handles
- Procs/Lambdas: Closures with bindings
- Methods: Method objects
- Threads: Thread objects
- System objects: Some built-in objects with special behavior
Advanced Marshal Features
Custom Marshalling with marshal_dump/marshal_load
class User
attr_accessor :name, :email, :password_hash
def initialize(name, email, password_hash)
@name = name
@email = email
@password_hash = password_hash
@login_count = 0
@last_login = nil
end
# Custom marshalling - exclude sensitive data
def marshal_dump
{
name: @name,
email: @email,
login_count: @login_count,
last_login: @last_login
# Intentionally exclude @password_hash for security
}
end
# Custom unmarshalling
def marshal_load(data)
@name = data[:name]
@email = data[:email]
@login_count = data[:login_count]
@last_login = data[:last_login]
@password_hash = nil # Must be set separately for security
end
def login!
@login_count += 1
@last_login = Time.now
end
def to_s
"#{@name} (#{@email}) - Logins: #{@login_count}"
end
end
# Usage
user = User.new("Alice", "alice@example.com", "hashed_password_123")
user.login!
user.login!
puts "Before marshal: #{user}"
# Marshal and unmarshal
marshalled = Marshal.dump(user)
restored_user = Marshal.load(marshalled)
puts "After marshal: #{restored_user}"
puts "Password hash preserved: #{restored_user.password_hash.nil?}" # true (for security)
Object References and Circular Dependencies
# Marshal handles object references and circular dependencies automatically
class Node
attr_accessor :value, :children, :parent
def initialize(value)
@value = value
@children = []
@parent = nil
end
def add_child(child_node)
child_node.parent = self
@children << child_node
end
def to_s
"Node(#{@value})"
end
end
# Create a tree structure with references
root = Node.new("root")
child1 = Node.new("child1")
child2 = Node.new("child2")
grandchild = Node.new("grandchild")
root.add_child(child1)
root.add_child(child2)
child1.add_child(grandchild)
puts "Original structure:"
puts "Root: #{root.value}"
puts "Children: #{root.children.map(&:value).join(', ')}"
puts "Grandchild parent: #{grandchild.parent.value}"
# Marshal preserves all relationships
marshalled_tree = Marshal.dump(root)
restored_root = Marshal.load(marshalled_tree)
puts "\nRestored structure:"
puts "Root: #{restored_root.value}"
puts "Children: #{restored_root.children.map(&:value).join(', ')}"
puts "Grandchild parent: #{restored_root.children[0].children[0].parent.value}"
# Verify object identity is preserved
restored_grandchild = restored_root.children[0].children[0]
puts "Parent reference correct: #{restored_grandchild.parent == restored_root.children[0]}"
# Even works with circular references
circular = Node.new("circular")
circular.parent = circular # Self-reference
circular.add_child(circular) # Add self as child
marshalled_circular = Marshal.dump(circular)
restored_circular = Marshal.load(marshalled_circular)
puts "Circular reference preserved: #{restored_circular.parent == restored_circular}"
Marshal Benefits
- Complete fidelity: Preserves exact object state and relationships
- Ruby-specific: Handles Ruby objects perfectly
- Efficient: Fast binary format
- Handles complexity: Circular references, deep nesting
- Version tracking: Includes Ruby version information
Security Considerations & Safe Alternatives
Security Vulnerabilities
# DANGEROUS: Never do this with untrusted data!
# This could execute arbitrary code
# Example of a malicious payload (DO NOT RUN)
# malicious_data = "\x04\x08o:\x0bKernel0\x06:\x06@" +
# "`system('rm -rf /')" # Malicious command
#
# Marshal.load(malicious_data) # Could execute rm -rf /
# Safe marshalling practices
class SafeMarshaller
TRUSTED_CLASSES = [
String, Integer, Float, Array, Hash, Symbol,
TrueClass, FalseClass, NilClass, Time, Date
].freeze
def self.safe_dump(object)
validate_object(object)
Marshal.dump(object)
end
def self.safe_load(data, source: :trusted)
case source
when :trusted
# Only for internal application data
Marshal.load(data)
when :external
# Never use Marshal for external data
raise SecurityError, "Cannot safely unmarshal external data"
else
raise ArgumentError, "Source must be :trusted or :external"
end
end
private
def self.validate_object(obj)
case obj
when *TRUSTED_CLASSES
# Basic types are safe
when Array
obj.each { |item| validate_object(item) }
when Hash
obj.each { |key, value| validate_object(key); validate_object(value) }
else
# Custom classes require explicit approval
unless safe_custom_class?(obj.class)
raise SecurityError, "Class #{obj.class} is not approved for marshalling"
end
end
end
def self.safe_custom_class?(klass)
# Define your approved custom classes
[User, Product, Order].include?(klass)
end
end
# Usage
data = { users: ["Alice", "Bob"], count: 2, active: true }
# Safe marshalling
marshalled = SafeMarshaller.safe_dump(data)
restored = SafeMarshaller.safe_load(marshalled, source: :trusted)
Safe Alternatives to Marshal
require 'json'
require 'yaml'
# JSON - Safe for simple data structures
data = {
name: "Alice",
age: 30,
skills: ["Ruby", "Rails", "JavaScript"],
active: true
}
# JSON serialization (safe but limited)
json_string = JSON.generate(data)
restored_from_json = JSON.parse(json_string, symbolize_names: true)
puts "JSON safe: #{restored_from_json}"
# YAML - Safe with YAML.safe_load
yaml_string = YAML.dump(data)
restored_from_yaml = YAML.safe_load(yaml_string, permitted_classes: [Symbol])
puts "YAML safe: #{restored_from_yaml}"
# Custom serialization for complex objects
class SerializableUser
attr_accessor :name, :email, :created_at
def initialize(name, email)
@name = name
@email = email
@created_at = Time.now
end
def to_safe_hash
{
'name' => @name,
'email' => @email,
'created_at' => @created_at.iso8601
}
end
def self.from_safe_hash(hash)
user = new(hash['name'], hash['email'])
user.created_at = Time.parse(hash['created_at'])
user
end
def serialize
JSON.generate(to_safe_hash)
end
def self.deserialize(json_string)
hash = JSON.parse(json_string)
from_safe_hash(hash)
end
end
# Usage
user = SerializableUser.new("Alice", "alice@example.com")
serialized = user.serialize
restored_user = SerializableUser.deserialize(serialized)
puts "Restored user: #{restored_user.name} (#{restored_user.email})"
# MessagePack - Alternative binary format (requires gem)
# gem 'msgpack'
#
# require 'msgpack'
#
# data = { name: "Alice", scores: [95, 87, 92] }
# packed = MessagePack.pack(data)
# unpacked = MessagePack.unpack(packed)
# puts "MessagePack: #{unpacked}"
Serialization Format Comparison
* YAML.safe_load only
Practical Applications
Object Caching System
class ObjectCache
def initialize(cache_dir = './cache')
@cache_dir = cache_dir
Dir.mkdir(@cache_dir) unless Dir.exist?(@cache_dir)
end
def set(key, object, ttl: 3600)
cache_file = cache_path(key)
cache_data = {
object: object,
created_at: Time.now,
ttl: ttl
}
File.open(cache_file, 'wb') do |file|
Marshal.dump(cache_data, file)
end
end
def get(key)
cache_file = cache_path(key)
return nil unless File.exist?(cache_file)
begin
cache_data = File.open(cache_file, 'rb') do |file|
Marshal.load(file)
end
# Check if expired
if expired?(cache_data)
delete(key)
return nil
end
cache_data[:object]
rescue => e
puts "Cache read error: #{e.message}"
delete(key)
nil
end
end
def delete(key)
cache_file = cache_path(key)
File.delete(cache_file) if File.exist?(cache_file)
end
def clear
Dir.glob(File.join(@cache_dir, '*')).each { |file| File.delete(file) }
end
private
def cache_path(key)
safe_key = key.to_s.gsub(/[^a-zA-Z0-9]/, '_')
File.join(@cache_dir, "#{safe_key}.cache")
end
def expired?(cache_data)
Time.now > cache_data[:created_at] + cache_data[:ttl]
end
end
# Usage
cache = ObjectCache.new
# Cache expensive computation result
class ExpensiveComputation
attr_reader :result, :computed_at
def initialize(data)
@data = data
@computed_at = Time.now
@result = perform_computation
end
private
def perform_computation
# Simulate expensive operation
sleep(0.1)
@data.map { |x| x ** 2 }.sum
end
end
data = (1..1000).to_a
computation = ExpensiveComputation.new(data)
# Cache the result
cache.set('expensive_computation', computation, ttl: 300) # 5 minutes
# Retrieve from cache
cached_result = cache.get('expensive_computation')
puts "Cached result: #{cached_result&.result}"
puts "Computed at: #{cached_result&.computed_at}"
Deep Object Cloning
class DeepCloner
def self.deep_clone(object)
Marshal.load(Marshal.dump(object))
end
end
# Example with complex nested structure
class Team
attr_accessor :name, :members, :projects
def initialize(name)
@name = name
@members = []
@projects = []
end
def add_member(member)
@members << member
end
def add_project(project)
@projects << project
end
end
class Member
attr_accessor :name, :role, :skills
def initialize(name, role)
@name = name
@role = role
@skills = []
end
end
class Project
attr_accessor :name, :status, :assigned_members
def initialize(name, status = "active")
@name = name
@status = status
@assigned_members = []
end
end
# Create original team structure
original_team = Team.new("Development Team")
alice = Member.new("Alice", "Developer")
alice.skills = ["Ruby", "Rails", "JavaScript"]
bob = Member.new("Bob", "Designer")
bob.skills = ["UI/UX", "Figma", "CSS"]
project1 = Project.new("Web App")
project1.assigned_members = [alice, bob]
original_team.add_member(alice)
original_team.add_member(bob)
original_team.add_project(project1)
# Deep clone the entire structure
cloned_team = DeepCloner.deep_clone(original_team)
# Verify independence
puts "Original team: #{original_team.name}"
puts "Cloned team: #{cloned_team.name}"
# Modify original
original_team.members[0].skills << "Python"
alice.name = "Alice Smith"
# Check that clone is unaffected
puts "Original Alice skills: #{original_team.members[0].skills}"
puts "Cloned Alice skills: #{cloned_team.members[0].skills}"
puts "Original Alice name: #{original_team.members[0].name}"
puts "Cloned Alice name: #{cloned_team.members[0].name}"
# Verify object identity is different
puts "Same object? #{original_team.members[0] == cloned_team.members[0]}"
State Persistence System
class GameState
attr_accessor :player_name, :level, :score, :inventory, :achievements
def initialize(player_name)
@player_name = player_name
@level = 1
@score = 0
@inventory = []
@achievements = []
@created_at = Time.now
end
def add_item(item)
@inventory << item
end
def unlock_achievement(achievement)
@achievements << achievement unless @achievements.include?(achievement)
end
def level_up!
@level += 1
@score += @level * 100
end
def save_to_file(filename = nil)
filename ||= "#{@player_name.downcase.gsub(/\s+/, '_')}_save.game"
File.open(filename, 'wb') do |file|
Marshal.dump(self, file)
end
puts "Game saved to #{filename}"
end
def self.load_from_file(filename)
unless File.exist?(filename)
puts "Save file not found: #{filename}"
return nil
end
begin
File.open(filename, 'rb') do |file|
Marshal.load(file)
end
rescue => e
puts "Error loading save file: #{e.message}"
nil
end
end
def stats
puts "=== Game Stats ==="
puts "Player: #{@player_name}"
puts "Level: #{@level}"
puts "Score: #{@score}"
puts "Inventory: #{@inventory.join(', ')}"
puts "Achievements: #{@achievements.join(', ')}"
puts "Created: #{@created_at}"
end
end
# Create and play game
game = GameState.new("Alice")
game.add_item("sword")
game.add_item("shield")
game.level_up!
game.unlock_achievement("First Level Up")
game.level_up!
game.unlock_achievement("Level 3 Master")
puts "Original game state:"
game.stats
# Save the game
game.save_to_file
# Load the game later
loaded_game = GameState.load_from_file("alice_save.game")
puts "\nLoaded game state:"
loaded_game&.stats
# Verify they're independent objects
game.level_up!
puts "\nAfter modifying original:"
puts "Original level: #{game.level}"
puts "Loaded level: #{loaded_game&.level}"
Best Practices & Guidelines
✅ Do's
- Use for internal application data only
- Implement custom marshal_dump/load for sensitive data
- Add version checks for compatibility
- Use binary mode ("wb"/"rb") for files
- Handle exceptions during load operations
- Consider TTL for cached data
- Test marshalling with your specific objects
❌ Don'ts
- Never marshal untrusted external data
- Don't use for API responses or web data
- Avoid marshalling objects with file handles
- Don't ignore version compatibility issues
- Never skip error handling
- Don't marshal extremely large objects
- Avoid marshalling thread-local data