Ruby Logo

Marshalling & Serialization

Understand Ruby marshalling, serialization security considerations, and safe alternatives for data persistence.

Home Ruby Marshalling & Serialization

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

Format
Security
Features
Use Case
Marshal
⚠️ Dangerous
Full Ruby
Internal only
JSON
✅ Safe
Basic types
APIs, configs
YAML
✅ Safe*
Human readable
Configurations
MessagePack
✅ Safe
Binary, efficient
Performance critical

* 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

Quick Navigation

Related Topics

Video Tutorial

Watch and learn marshalling & serialization

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!