skip to content

In Ruby 4.0, how do YAML.load, YAML.safe_load and YAML.unsafe_load differ, and why does loading a config containing a date raise Psych::DisallowedClass?

level: seniorimportance: must knowfreq 55%

answer

  1. Psych 4, Ruby 3.1
  2. load is safe_load plus Symbol
  3. permitted_classes: [Date]
  4. aliases: false by default
  5. unsafe_load builds any class

basics

~20 s

Since Psych 4 (Ruby 3.1), YAML.load is safe_load that also permits Symbol: only plain scalars, arrays and hashes load, and aliases are off. A date value would build a Date, which is not permitted, so pass permitted_classes: [Date]. unsafe_load builds any tagged class.

solid answer

~40 s

`require "yaml"` makes `YAML` an alias of `Psych` (5.3.1 in Ruby 4.0.7). Since **Psych 4, shipped with Ruby 3.1**, `YAML.load` calls `safe_load` with `permitted_classes: [Symbol]`: it builds only `nil`, booleans, `Integer`, `Float`, `String`, `Array`, `Hash` and Symbols, and rejects aliases unless `aliases: true`. `YAML.safe_load` is the same without Symbol. An unquoted scalar like `2026-09-30` resolves to a `Date`, which is not on the list, so the load raises `Psych::DisallowedClass` ("Tried to load unspecified class: Date"); pass `permitted_classes: [Date]`, and note that giving `load` your own list replaces its `[Symbol]` default. `YAML.unsafe_load` is the old unrestricted loader, which builds any class a `!ruby/object` tag names, so it is only for files you wrote yourself. Anchors and aliases need `aliases: true`, otherwise `Psych::AliasesNotEnabled`.

code

ruby · 16 lines
ruby
require "yaml"
require "date"

text = <<~YAML
  currency: EUR
  season_starts: 2026-09-30
YAML

YAML.load(text)
# Psych::DisallowedClass: Tried to load unspecified class: Date

YAML.load(text, permitted_classes: [Date], symbolize_names: true)
# => {currency: "EUR", season_starts: #<Date: 2026-09-30 ...>}

YAML.load("base: &b {a: 1}\nother: *b")
# Psych::AliasesNotEnabled (pass aliases: true for trusted files)

go deeper

for a junior

Recall that YAML is Psych, that YAML.load is safe by default, and that unsafe_load exists for trusted files only.

for a middle

Explain what safe_load permits, why an unquoted date raises DisallowedClass, and how permitted_classes and aliases: true fix it.

for a senior

Audit YAML loading by input source, keep unsafe_load to self-written files, and know that a custom permitted_classes list drops the Symbol default.

for a principal

Decide which formats may carry Ruby types between systems, and keep configuration YAML to plain data with explicit, reviewed permitted classes.

## One library, three loaders `require "yaml"` loads **Psych**, Ruby's YAML library, and sets `YAML = Psych`. Ruby 4.0.7 ships Psych 5.3.1 as a default gem. Psych's loaders differ in which Ruby classes a document may produce: | Loader | Classes it may build | Aliases | Typical use | |---|---|---|---| | `YAML.safe_load` | `TrueClass`, `FalseClass`, `NilClass`, `Integer`, `Float`, `String`, `Array`, `Hash`, plus `permitted_classes:` | off unless `aliases: true` | anything from outside | | `YAML.load` | the same **plus `Symbol`** by default | off unless `aliases: true` | configuration files | | `YAML.unsafe_load` | any class, including those named by `!ruby/object:` tags | on | only YAML your own code wrote | The file variants follow the same pattern: `YAML.load_file`, `YAML.safe_load_file` and `YAML.unsafe_load_file` open the file (stripping a byte-order mark) and call the matching loader with the file name for error messages. ## What changed in Ruby 3.1 Before Psych 4, `YAML.load` was the unrestricted loader. **Psych 4.0, shipped with Ruby 3.1, changed `load` to behave as `safe_load` by default**, and the old behaviour moved to `unsafe_load`. Code and blog posts from before that change often call `YAML.load` on YAML that contains Ruby objects; on current Ruby those loads fail with `Psych::DisallowedClass`, and the right fix is almost never a blind switch to `unsafe_load`. ## Why a date breaks the load YAML resolves untagged scalars by their form. An unquoted value matching `YYYY-MM-DD` is resolved to a **`Date`**, and a full timestamp to a `Time`. So this catalogue configuration ```yaml catalogue: currency: EUR season_starts: 2026-09-30 ``` asks Psych to create a `Date`. `Date` is not on the safe list, so `YAML.load` raises `Psych::DisallowedClass` with the message "Tried to load unspecified class: Date". Three fixes, from most to least common: 1. **Permit it**: `YAML.load_file("catalogue.yml", permitted_classes: [Date])`. The list is additive to the basic types, but it **replaces** `load`'s default `[Symbol]`, so write `permitted_classes: [Date, Symbol]` if the file also uses Symbols. 2. **Quote it** in the file (`season_starts: "2026-09-30"`) and parse the String yourself. 3. **Never** reach for `unsafe_load` just to get a `Date` through. ## Aliases YAML **anchors and aliases** (`&defaults` and `*defaults`, often with `<<:` merge keys) let one node be reused. Both `safe_load` and `load` default to `aliases: false` and raise `Psych::AliasesNotEnabled`, a subclass of `Psych::BadAlias`, when a document uses them. Pass `aliases: true` for trusted configuration that relies on them. The default is off because aliases can reference a node many times, and a small document can expand into a very large structure. ## Useful options - `symbolize_names: true` returns Symbol keys, as in JSON parsing, without needing `Symbol` in `permitted_classes`. - `freeze: true` returns deeply frozen objects. - `permitted_symbols:` restricts which Symbols may appear when `Symbol` is permitted. - `fallback:` is what an empty document returns: `nil` for `load` and `safe_load`. - Syntax errors raise `Psych::SyntaxError`, which carries the file name when you pass `filename:` or use a `*_file` method. ## Dumping is the other half `YAML.dump(obj)` writes any Ruby object, tagging non-basic classes with `!ruby/object:` so that `unsafe_load` can rebuild them. That is how unsafe documents usually come into existence: someone dumped an object, not a Hash. `YAML.safe_dump(obj)` is the restricted counterpart: it accepts only the basic types plus what `permitted_classes:` and `permitted_symbols:` allow, and raises `Psych::DisallowedClass` ("Tried to dump unspecified class") for anything else, so a file it wrote is guaranteed to load with `safe_load`. Pair `safe_dump` with `safe_load` when you control both ends. ## Choosing a loader - A configuration file checked into the repository: `YAML.load_file(path, permitted_classes: [Date])`, plus `aliases: true` if it uses anchors. - YAML from users, uploads, other services or a database column users can write: `YAML.safe_load`, with the narrowest `permitted_classes` list that works. - A file your own process wrote with `YAML.dump` to round-trip Ruby objects: `YAML.unsafe_load`, and only while that file cannot be modified by anyone else. `JSON` or explicit Hashes are usually a better format for that job. How YAML's implicit typing turns `no` or `NO` into `false` is a separate trap of the format itself, and it applies to all three loaders equally.

  • Why does YAML.load(text, permitted_classes: [Date]) suddenly reject Symbols?
    `YAML.load`'s default for `permitted_classes` is `[Symbol]`. Passing your own list replaces that default rather than adding to it, so Symbols are no longer allowed. Write `permitted_classes: [Date, Symbol]` when the document also contains Symbol values.
  • An older snippet calls YAML.load on a file with !ruby/object tags and now fails; what do you do?
    Check where the file comes from. If your own code wrote it and nobody else can change it, `YAML.unsafe_load_file` restores the old behaviour. Otherwise permit the specific classes it needs with `permitted_classes:`, or better, change the file to plain Hashes and build the objects in code.

safe_load is a venue door with a guest list: plain guests get in, and a Date is turned away until you add it to the list. unsafe_load removes the door, so anyone with a costume naming a class walks in as that class.

saying these in an interview costs you the question

  • YAML.load is still the unrestricted loader in current Ruby
  • Passing permitted_classes to YAML.load adds to its Symbol default
  • Anchors and aliases load by default with safe_load
  • Switching to unsafe_load is the standard fix for DisallowedClass
  • Quoting a date in YAML still produces a Date object