skip to content

In Ruby, what can Marshal.dump not serialize, what must exist before Marshal.load can rebuild an object, and when is Marshal the wrong choice?

level: seniorimportance: should knowfreq 28%

answer

  1. binary, format version 4.8
  2. no Proc, IO or singleton
  3. no Hash with a default proc
  4. class must be defined on load
  5. never load untrusted bytes

basics

~20 s

Marshal.dump refuses procs, methods, IO objects, anonymous classes, objects with singleton methods and hashes with a default proc, raising TypeError. Marshal.load needs every class defined, or raises ArgumentError. It is wrong for untrusted bytes and for data read by other programs.

solid answer

~40 s

`Marshal.dump(obj)` writes a binary snapshot of an object graph, including class names and instance variables, and `Marshal.load` rebuilds it. The format carries its own version, 4.8, which is independent of Ruby's version number. Dumping raises `TypeError` for `Proc`, `Method`, `IO` and `File`, `Thread`, anonymous classes, objects that have singleton methods, and a `Hash` with a default proc. Loading resolves every class by name, so each must already be defined; otherwise `ArgumentError` ("undefined class/module Product") is raised, and renamed or removed classes break old snapshots. Classes can control their state with `marshal_dump` and `marshal_load`. `Marshal.load(Marshal.dump(obj))` is the classic deep-copy idiom. Ruby's documentation says never to pass untrusted data to `Marshal.load`, because it can build almost any loaded class; data that crosses a trust or language boundary belongs in JSON.

code

ruby · 12 lines
ruby
Product = Struct.new(:sku, :tags)
lamp  = Product.new("LMP-01", ["home"])

bytes = Marshal.dump(lamp)
bytes[0, 2].bytes              # => [4, 8]  format version

copy = Marshal.load(bytes)
copy.tags << "sale"
lamp.tags                      # => ["home"]  deep copy

Marshal.dump(-> { 1 })                   # TypeError: no _dump_data is defined for class Proc
Marshal.dump(Hash.new { |h, k| h[k] = [] })  # TypeError: can't dump hash with default proc

go deeper

for a junior

Recall that Marshal.dump and Marshal.load serialize Ruby objects to binary, and that the deep-copy idiom uses both.

for a middle

List what cannot be dumped, including default-proc hashes, and explain why loading needs every class defined by name.

for a senior

Keep Marshal away from anything an outsider can write, and from caches that outlive class renames; choose JSON across trust boundaries.

for a principal

Set a policy for serialized state: which stores may hold Ruby-specific snapshots, how they are invalidated on deploy, and which must be JSON.

## What Marshal is `Marshal` is Ruby's built-in **binary serializer**. `Marshal.dump(obj)` returns a `String` (or writes to an `IO`) describing `obj` and everything it references: class names, instance variables, shared references and cycles. `Marshal.load(bytes)` reads it back into new objects. It is part of core Ruby, needs no `require`, and is fast. Every dump starts with two bytes, the **format version**: major 4, minor 8. The documentation stresses that "Marshal versioning is independent of Ruby's version numbers": the format itself has been stable for a long time. What changes between releases and deploys is your **classes**, and that is where compatibility breaks. ## What cannot be dumped `Marshal.dump` raises `TypeError` for anything whose state is not plain data: | Object | Why | |---|---| | `Proc`, lambda, `Method`, `UnboundMethod`, `Binding` | code and closures, not data | | `IO`, `File`, `Dir`, sockets, `File::Stat` | tied to operating-system resources | | `Thread`, `ThreadGroup`, `MatchData` | runtime state | | an anonymous `Class` or `Module` | no name to write down | | any object with **singleton methods** | per-object methods cannot be restored | | a `Hash` with a **default proc** | "can't dump hash with default proc" | The last row is the one that surprises people most: a cache holding `Hash.new { |h, k| h[k] = [] }` cannot be marshalled at all, while `Hash.new(0)` with a plain default value can. ## What loading requires `Marshal.load` rebuilds each object by looking its **class up by name**: 1. The class must be **defined** in the loading process. If `Catalogue::Product` is not loaded yet, or was renamed, the load raises `ArgumentError` with "undefined class/module Catalogue::Product". 2. Instance variables are restored **as they were**. The object is allocated and filled in without calling `initialize`, so any validation there is skipped, and a class whose fields changed since the dump gets the old set. 3. Classes that define **`marshal_dump`** return whatever data they choose, and their **`marshal_load(data)`** is called on a freshly allocated object; the older `_dump`/`self._load` pair does the same with a String. 4. `Marshal.load(bytes, proc)` calls the proc on each object as it is loaded, and `freeze: true` returns deeply frozen objects with deduplicated strings. Point 1 is why marshalled data in a **cache shared across deploys** is fragile: a deploy that renames or moves a class makes every old entry unreadable, and a deploy that changes a class's fields makes old entries load with stale state. ## Keeping unserializable parts out A class that holds something Marshal cannot dump, such as a logger or a memoizing proc, can still be marshalled by defining the hooks: - `marshal_dump` returns only the plain data, for example `[@sku, @price_cents]`; - `marshal_load(data)` restores those fields and rebuilds the rest, such as recreating the proc. The same hooks are the place to add a small version number to the dumped data, so a newer class can recognise and convert an older snapshot instead of failing. ## Trust The `Marshal` documentation is explicit: `Marshal.load` "can deserialize almost any class loaded into the Ruby process", and "you should never unmarshal user supplied input or other untrusted data". That rules out cookies, uploaded files, request parameters and any cache or queue an attacker can write to. The general theory of why letting input choose classes is dangerous belongs to application security; the Ruby fact to remember is that `Marshal.load` has no allow-list option at all, unlike `YAML.safe_load`. ## When Marshal is a good fit, and when not Good fits: - **Deep copies**: `Marshal.load(Marshal.dump(obj))` copies a nested structure of plain objects, where `dup` and `clone` are shallow. - **Process-local or same-version caches** that only your code writes and reads, short-lived, and cleared on deploy. - **Passing plain data to a forked child** or between processes running the same code. Wrong choices: - anything an outside party can write, for the trust reason above; - data read by programs in other languages, since the format is Ruby-only; - long-lived storage, which outlives class renames and field changes; - objects containing procs, IO or default-proc hashes, which will not dump. For the product catalogue, marshalling the parsed catalogue into a local cache file between runs of the same importer is reasonable; sending it to a partner or storing it in a shared cache that other services read is not, and JSON is the format for that.

  • Why does a shared cache of marshalled objects start raising ArgumentError after a refactor moved a class into a namespace?
    Each cached entry stores the old constant name. `Marshal.load` resolves classes by that name, finds none, and raises `ArgumentError: undefined class/module OldName`. Versioning the cache key per deploy, or storing plain Hashes instead of objects, avoids it.
  • Does Marshal.load run initialize?
    Not by default. It allocates each object and restores its instance variables directly, or calls `marshal_load` when the class defines it; only a class-level `_load` that itself calls `new` goes through `initialize`. Invariants that `initialize` enforces are not checked, so a snapshot can produce an object its constructor would have rejected.

saying these in an interview costs you the question

  • Marshal output is portable across programming languages
  • Marshal.load calls initialize on each object
  • Any Hash can be dumped, whatever its default
  • The Marshal format changes with every Ruby release
  • Marshal.load accepts an allow-list of classes like safe_load