skip to content

In Ruby 4.0, whose default json gem is 2.18, how does JSON.load differ from JSON.parse, and which belongs on untrusted input?

level: middleimportance: should knowfreq 32%

answer

  1. load takes an IO or nil
  2. allow_blank and allow_nan on
  3. json_class and json_create
  4. warning hidden by default
  5. load_file is parse-based

basics

~20 s

JSON.load is a lenient loader for trusted data: it reads IOs, maps empty input to nil, accepts NaN and, in json 2.18, builds any class a json_class key names that defines json_create. Untrusted input belongs in JSON.parse.

solid answer

~40 s

`JSON.parse(string, **opts)` is the strict entry point: a `String` in, core objects out, `NaN` rejected, nesting capped at 100, and no class instantiation. `JSON.load(source, proc = nil, opts)` is the counterpart of `JSON.dump` and is lenient: it accepts an `IO` (anything with `read`, `to_io` or `to_str`), turns `nil` or `""` into `nil` (`allow_blank`), accepts `NaN` (`allow_nan`), can call a `proc` on each value, and in json 2.18, the version Ruby 4.0.7 ships, still honours **create additions**: an object carrying a `"json_class"` key makes it call that class's `json_create`. That path is deprecated since json 2.8, but its warning is in the `:deprecated` category, which is off by default, so it is silent. json 3.0 switches it off. For anything from outside the process, use `JSON.parse`, or `JSON.load_file`, which despite its name calls `parse`.

code

ruby · 12 lines
ruby
require "json"
require "json/add/core"            # defines Range.json_create among others

doc = '{"json_class": "Range", "a": [1, 10, false]}'

JSON.parse(doc)   # => {"json_class" => "Range", "a" => [1, 10, false]}
JSON.load(doc)    # => 1..10  (json 2.18: deprecation warning only with -W:deprecated)

JSON.load(nil)    # => nil   allow_blank
JSON.parse("")    # JSON::ParserError

JSON.load(doc, nil, create_additions: false)  # => the plain Hash

go deeper

for a junior

Recall that JSON.parse is the default choice for text you receive, and that JSON.load is the reader for JSON.dump output.

for a middle

Explain JSON.load's lenient defaults, allow_blank, allow_nan and create_additions, and why JSON.load_file is parse-based despite its name.

for a senior

Audit JSON.load and unsafe_load call sites against their input sources, and surface hidden json deprecation warnings in CI with -W:deprecated.

for a principal

Plan the move to json 3.0's defaults: explicit options everywhere, no reliance on mutable globals, and additions confined to self-written data.

## Two entry points with different jobs The json library has a strict parser and a convenience loader: | | `JSON.parse` | `JSON.load` | |---|---|---| | Accepts | a `String` | a `String`, an `IO`, anything with `to_str`, `to_io` or `read` | | `nil` or `""` | raises (`TypeError` or `JSON::ParserError`) | returns `nil` (`allow_blank: true`) | | `NaN`, `Infinity` | rejected (`allow_nan: false`) | accepted (`allow_nan: true`) | | Nesting limit | 100 (`max_nesting`) | 100 | | `"json_class"` key | ignored, an ordinary key | may build that class (json 2.18) | | Per-value hook | through the parse options | the optional `proc` argument | `JSON.load` exists to read back what `JSON.dump` wrote. `JSON.dump` is its mirror: it writes to an optional `IO` and emits `NaN` rather than raising. ## Create additions: the part that matters The json gem has an old feature called **additions**. A class can define a class method `json_create(hash)`, and files under `json/add/` (for example `json/add/core`) define it for core classes such as `Range`, `Date` and `Struct`. When a loader runs with **`create_additions`** enabled and meets an object like `{"json_class": "Range", "a": [1, 10, false]}`, it resolves the constant named by `json_class` and calls its `json_create` with the hash. In json **2.18.0**, the version Ruby 4.0.7 ships as a default gem, the options differ by entry point: - `JSON.parse` defaults `create_additions` to `false`, so `json_class` is just a key. - `JSON.load` defaults it to `nil`, meaning "deprecated but still on": if a document names a class that responds to `json_create`, the loader builds it and calls `JSON.deprecation_warning`. - `JSON.unsafe_load` sets it to `true` explicitly, and also disables the nesting limit. That deprecation warning is emitted with `category: :deprecated`, and Ruby hides that category unless you enable it (`-W:deprecated` or `Warning[:deprecated] = true`). In an ordinary production process, `JSON.load` builds objects from a document's type hints **without printing anything**. Which classes can be built depends on what the process has loaded and which of them define `json_create`, which is exactly why the library's own documentation says `JSON.load` "is meant to serialise data from trusted user input" and suggests `JSON.unsafe_load` to make the risk visible. Why letting input choose classes is dangerous in general is a security topic of its own; the Ruby-specific point is which entry point enables it. ## What changes with json 3.0 The standalone json 3.0 release, which an application only gets by adding it to its Gemfile, removes the `create_additions` path from `JSON.load`, removes the mutable default-option globals such as `JSON.load_default_options`, and raises `ArgumentError` for unknown options. `JSON.unsafe_load` remains for code that really wants additions. Code written for Ruby 4.0.7's bundled 2.18 should not rely on either default: pass options explicitly or use `JSON.parse`. ## Choosing, in practice 1. **Request bodies, webhooks, uploaded files, third-party APIs**: `JSON.parse(text)` or `JSON.load_file(path)`. `JSON.load_file` reads the file as UTF-8 and calls `parse`, so it is the strict path despite its name. 2. **Your own cache or fixture written with `JSON.dump`**: `JSON.load` is acceptable, but passing `create_additions: false` costs nothing and removes the surprise. 3. **Deliberately round-tripping Ruby types through JSON**: say so with `JSON.unsafe_load`, and keep it to data your process wrote itself. ## The proc argument `JSON.load(source, proc)` calls `proc` on every value as it is completed, depth-first, and returns the parsed result. It is a hook for inspecting or post-processing values, for example collecting every `"sku"` seen while loading a fixture. It does not replace validation: the objects already exist when the proc sees them. Passing options as the second argument also works, because `JSON.load` treats a Hash in the proc position as the options. ## Reading an IO A common reason to reach for `JSON.load` is that it takes a file handle. `JSON.parse(io.read)` or `JSON.load_file(path)` gives the same convenience on the strict path, so leniency never has to be the price of reading from a stream. ## Checklist - Search the codebase for `JSON.load(` and `JSON.unsafe_load(` and check where each input comes from. - Run tests with `-W:deprecated` so the json deprecation warnings become visible in CI. - Prefer explicit options over the mutable defaults, which json 2.18 already marks as deprecated.

  • Why does a team never see the create_additions deprecation warning in production logs?
    The json gem emits it with `category: :deprecated`, and Ruby suppresses that category unless deprecation warnings are enabled, for example with `-W:deprecated` or `Warning[:deprecated] = true`. Enabling it in the test suite surfaces every `JSON.load` that actually built an object from a type hint.
  • Is JSON.load_file the lenient loader for files?
    No. Despite the name, `JSON.load_file(path, opts)` reads the file as UTF-8 and calls `JSON.parse`, so it has parse's strict defaults and never builds objects from `json_class`. It is the right call for reading an untrusted JSON file.

saying these in an interview costs you the question

  • JSON.load and JSON.parse are aliases with identical defaults
  • JSON.load_file uses JSON.load, so it is unsafe for uploads
  • JSON.load would print a warning if it built an object, so silence proves safety
  • JSON.parse builds a class named by a json_class key
  • Ruby 4.0 ships json 3.0, so JSON.load no longer builds objects