Ruby Marshal Serialization — dump load and Object Persistence Explained
In this tutorial, you will learn about Ruby Marshal Serialization. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby Marshal provides binary serialization for saving and restoring Ruby objects using dump to serialize and load to deserialize, supporting built-in types and custom objects with Marshal.dump and Marshal.load.
What You'll Learn
- Serializing objects with Marshal.dump
- Deserializing with Marshal.load
- Safety considerations with untrusted data
- Custom serialization with marshal_dump/marshal_load
Why It Matters
Serialization is fundamental for persistence. Durga Antivirus Pro uses Marshal to cache scan results and quarantine metadata. Doda Browser uses Marshal for bookmark and session persistence. Marshal is built into Ruby — no gems required.
Real-World Use
Redis caching with marshalled objects, file-based persistence for desktop apps, session storage in Rails (before alternative stores), deep cloning objects, saving application state between runs.
flowchart LR
A["Marshal"] --> B["dump"]
B --> C["load"]
C --> D["Security"]
D --> E["Custom"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic Serialization
require "pp"
# Serialize simple types
data = {
name: "Alice",
age: 30,
skills: ["Ruby", "Rails", "SQL"],
active: true
}
serialized = Marshal.dump(data)
puts serialized.inspect # Binary string (\x04\b{\t...)
# Deserialize
restored = Marshal.load(serialized)
puts restored == data # true
puts restored[:name] # Alice
Save to File
# Write
data = { status: "ok", count: 42 }
File.open("data.marshal", "wb") { |f| f.write(Marshal.dump(data)) }
# Read
loaded = Marshal.load(File.binread("data.marshal"))
puts loaded.inspect # {:status=>"ok", :count=>42}
Supported Types
Marshal supports most Ruby objects natively:
supported = {
nil: nil,
boolean: [true, false],
numbers: [42, 3.14, 1e10, 1r/3, 1+2i],
strings: ["hello", :symbol],
arrays: [[1, 2, 3], [1, [2, 3]]],
hashes: [{a: 1, b: 2}, {1 => "a", [2] => "b"}],
ranges: [1..10, "a".."z"],
regexp: [/hello/i, /^\d+$/],
time: Time.now,
structs: Struct.new(:x, :y).new(1, 2)
}
supported.each do |type, values|
values.each do |val|
restored = Marshal.load(Marshal.dump(val))
puts "#{type}: #{val.inspect} => #{restored.inspect}"
end
end
Unsupported Types
Some objects cannot be marshalled:
# These raise TypeError
# Marshal.dump(Proc.new { "hello" }) # Proc
# Marshal.dump(-> { "hello" }) # Lambda
# Marshal.dump(IO.new(0)) # IO
# Marshal.dump(Binding.new) # Binding
# Marshal.dump(Thread.new {}) # Thread
begin
Marshal.dump(Proc.new { "hello" })
rescue TypeError => e
puts e.message # can't dump: Proc
end
Custom Serialization with marshal_dump/marshal_load
For custom classes, define marshal_dump and marshal_load:
class User
attr_reader :name, :password_hash
def initialize(name, password)
@name = name
@password_hash = password.hash
@password = password # Sensitive — don't serialize!
end
def marshal_dump
{ name: @name, password_hash: @password_hash }
end
def marshal_load(data)
@name = data[:name]
@password_hash = data[:password_hash]
@password = nil # Never restore the plain password
end
def authenticate?(password)
password.hash == @password_hash
end
end
user = User.new("Alice", "secret123")
data = Marshal.dump(user)
restored = Marshal.load(data)
puts restored.name # Alice
puts restored.authenticate?("secret123") # true
puts restored.authenticate?("wrong") # false
Using _dump and _load
Alternative approach for compact serialization:
class Point
attr_reader :x, :y
def initialize(x, y)
@x, @y = x, y
end
def _dump(level)
[@x, @y].pack("EE")
end
def self._load(data)
x, y = data.unpack("EE")
new(x, y)
end
end
p = Point.new(3.14, 2.72)
dump = Marshal.dump(p)
restored = Marshal.load(dump)
puts "x=#{restored.x}, y=#{restored.y}" # x=3.14, y=2.72
Deep Cloning with Marshal
Marshal is the most common way to deep clone objects:
original = { a: [1, 2, { b: "hello" }], c: [3, 4, 5] }
clone = Marshal.load(Marshal.dump(original))
# Modify nested structure
clone[:a][2][:b] = "world"
puts original[:a][2][:b] # "hello" — original unchanged
puts clone[:a][2][:b] # "world"
# Compare with dup
shallow = original.dup
shallow[:a][2][:b] = "modified"
puts original[:a][2][:b] # "modified" — shallow copy!
Security Warning
NEVER use Marshal.load with untrusted data:
# Dangerous! Attacker can inject arbitrary objects
# data = gets.chomp.unpack1("m") # From user input
# obj = Marshal.load(data) # RCE vulnerability!
# Safe alternative — use JSON or YAML for untrusted data
require "json"
safe = JSON.parse(File.read("trusted.json"))
# For partial safety with Marshal, use allowlist (Ruby 3.1+)
permitted = [Symbol, String, Integer, Array, Hash]
# Marshal.load(data, permitted_classes: permitted)
Why Marshal is Dangerous
class Malicious
def initialize
@payload = `rm -rf /` # Never actually run this!
end
end
# If an attacker crafts a marshal stream that creates Malicious,
# the instance's initialize runs arbitrary code.
# Marshal.load(attacker_data) # RCE!
Marshal vs JSON vs YAML
require "json"
require "yaml"
data = { name: "Alice", nested: { a: [1, 2, 3] } }
# Marshal — fastest, Ruby-only, binary
marshal = Marshal.dump(data)
puts "Marshal: #{marshal.bytesize} bytes" # ~35
# JSON — portable, slowest for Ruby objects
json = JSON.generate(data)
puts "JSON: #{json.bytesize} bytes" # ~36
# YAML — human-readable, slow
yaml = YAML.dump(data)
puts "YAML: #{yaml.bytesize} bytes" # ~55
Marshal with Singleton Methods
obj = "hello"
def obj.greet
"Hi from singleton!"
end
# Marshal cannot restore singleton methods
begin
Marshal.dump(obj)
rescue TypeError => e
puts e.message # singleton can't be dumped
end
# Workaround: remove singleton method first
obj.singleton_class.send(:remove_method, :greet)
serialized = Marshal.dump(obj)
Common Mistakes
1. Loading Untrusted Marshal Data
# NEVER do this
# user_input = params[:data].unpack1("m")
# config = Marshal.load(user_input)
# Always validate source
unless File.exist?("trusted_config.marshal")
raise "Configuration file not found!"
end
2. Forgetting Binary File Mode
# Wrong — text mode corrupts binary data
File.write("data.marshal", Marshal.dump(obj))
# Correct — binary mode
File.binwrite("data.marshal", Marshal.dump(obj))
3. Marshalling Unsupported Types
# Will raise TypeError at runtime
obj = { callback: ->(x) { x * 2 } }
# Marshal.dump(obj) # TypeError
4. Assuming Marshal is Portable
# Marshal format changes between Ruby versions
# Data dumped with Ruby 3.0 might not load on Ruby 3.3
# Always test cross-version compatibility
5. Using Marshal for Deep Cloning Without Understanding
# Cloning hides state changes
class SingletonDB
def self.instance
@instance ||= new
end
end
# Marshal.clone creates a new instance — breaks singleton pattern!
clone = Marshal.load(Marshal.dump(SingletonDB.instance))
puts clone == SingletonDB.instance # false
Practice Questions
1. What does Marshal.dump do?
Serializes a Ruby object into a binary string. The object must be composed of marshallable types (no Procs, IOs, or singleton methods).
2. Why is Marshal.load dangerous with untrusted data?
It can create arbitrary Ruby objects, triggering malicious initialize methods or exploiting object deserialization for remote code execution.
3. How do you customize serialization for a class?
Define marshal_dump (returns a simplified representation) and marshal_load(data) (restores the object from that representation).
4. What's the difference between Marshal and JSON?
Marshal is Ruby-specific, faster, preserves object structure, but is not portable. JSON is cross-platform, slower for complex objects, and doesn't preserve Ruby-specific types.
Challenge: Create a Cache class that stores marshalled data in memory with a TTL (time-to-live) and auto-expiry.
Solution
class Cache
def initialize
@store = {}
end
def set(key, value, ttl_seconds = 300)
@store[key] = {
value: Marshal.dump(value),
expires_at: Time.now + ttl_seconds
}
end
def get(key)
entry = @store[key]
return nil unless entry
return nil if Time.now > entry[:expires_at]
Marshal.load(entry[:value])
rescue TypeError
nil
end
def delete(key)
@store.delete(key)
end
def cleanup
@store.delete_if { |_, entry| Time.now > entry[:expires_at] }
end
def size
@store.size
end
end
cache = Cache.new
cache.set("user_1", { name: "Alice", role: "admin" }, 2)
puts cache.get("user_1")[:name] # Alice
sleep(3)
puts cache.get("user_1") # nil (expired)
FAQ
{{< faq question="Is Marshal faster than JSON?" >}} Yes. Marshal is significantly faster for Ruby objects because it's in native C code and doesn't need to convert to a text format. However, JSON is safer for untrusted data. {{< /faq >}}
{{< faq question="Can I use Marshal for caching?" >}} Yes. Marshal is excellent for caching Ruby objects in memory or file stores. Combined with Redis or Memcached, it's a common pattern in Rails applications. {{< /faq >}}
{{< faq question="Does Marshal work across Ruby versions?" >}} Generally yes for minor versions, but major version changes can break compatibility. Always test serialization when upgrading Ruby. Version markers in the dump format help detect incompatibility. {{< /faq >}}
{{< faq question="How do I handle circular references?" >}} Marshal handles circular references automatically. It tracks already-serialized objects and uses back-references to avoid infinite loops. {{< /faq >}}
{{< faq question="What's the maximum size for Marshal data?" >}} No hard limit beyond available memory. Marshal loads the entire stream into memory, so very large objects may cause memory issues. {{< /faq >}}
Try It Yourself
# marshal_demo.rb
require "date"
class Note
attr_reader :title, :body, :created_at, :tags
def initialize(title, body, tags = [])
@title = title
@body = body
@created_at = Time.now
@tags = tags
end
def marshal_dump
{ title: @title, body: @body, created_at: @created_at, tags: @tags }
end
def marshal_load(data)
@title = data[:title]
@body = data[:body]
@created_at = data[:created_at]
@tags = data[:tags]
end
end
notes = [
Note.new("First post", "Hello world!", ["ruby"]),
Note.new("Marshal guide", "How to serialize", ["ruby", "serialization"])
]
# Serialize all notes
File.binwrite("notes.marshal", Marshal.dump(notes))
# Deserialize
restored = Marshal.load(File.binread("notes.marshal"))
restored.each do |note|
puts "#{note.title} (#{note.tags.join(', ')})"
end
What's Next
Now that you understand serialization, learn about logging in Ruby for debugging and monitoring applications.
| Topic | Description | Link |
|---|---|---|
| Ruby Logging | Logger, log levels, formatting | {{< ref "24-logging" >}} |
| Ruby Exception Handling | begin/rescue/ensure | {{< ref "20-exception-handling" >}} |
| Python Pickle | Compare Python's pickle module | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro