skip to content

In Ruby, how do JSON.generate, JSON.pretty_generate and to_json differ, and what happens when you serialize an object of your own class?

level: middleimportance: should knowfreq 42%

answer

  1. compact vs two-space indented
  2. to_json added by require "json"
  3. fallback calls to_s
  4. pass *args through to_json
  5. strict: true and GeneratorError

basics

~10 s

JSON.generate returns compact JSON, JSON.pretty_generate the same with two-space indentation, and to_json is the per-object hook the generator calls. An object without its own to_json is silently written as its to_s string.

solid answer

~40 s

All three produce a JSON `String`. `JSON.generate(obj)` is compact; `JSON.pretty_generate(obj)` adds newlines, two-space indentation and `": "`; `obj.to_json` is the method `require "json"` adds to core classes, and the generator calls it for any value that is not a native JSON type. Symbols become strings and `nil` becomes `null`. For your own class, the generic fallback `Object#to_json` calls `to_s`, so a `Product` is written as `"#<Product:0x...>"` without any error. Define `to_json(*args)` returning `to_h.to_json(*args)`, passing the arguments on so pretty-printing and depth tracking reach nested values, or use `JSON::Coder` with a block, which runs in `strict` mode. `strict: true` makes unsupported types raise `JSON::GeneratorError`, and `NaN` or `Infinity` raise it by default too.

code

ruby · 14 lines
ruby
require "json"

Product = Data.define(:sku, :price_cents)
lamp = Product.new(sku: "LMP-01", price_cents: 2450)

JSON.generate([lamp])               # => "[\"#<data Product sku=\\\"LMP-01\\\", price_cents=2450>\"]"
JSON.generate([lamp], strict: true) # JSON::GeneratorError

class Product
  def to_json(*args) = to_h.to_json(*args)
end

JSON.generate([lamp])        # => "[{\"sku\":\"LMP-01\",\"price_cents\":2450}]"
puts JSON.pretty_generate({items: [lamp]})

go deeper

for a junior

Recall that generate is compact, pretty_generate is indented, and to_json exists once json is required.

for a middle

Explain the to_s fallback for unknown objects, why a custom to_json forwards *args, and what strict: true changes.

for a senior

Design exports that convert values explicitly, use JSON::Coder or strict mode so unconverted types fail in tests, and format money and time deliberately.

for a principal

Choose one serialization boundary per service, a coder or explicit presenters, instead of to_json methods scattered across domain classes.

## Three ways to get a JSON string After `require "json"`: | Call | Output for `{sku: "LMP-01", tags: [:home]}` | |---|---| | `JSON.generate(h)` | `{"sku":"LMP-01","tags":["home"]}` | | `JSON.pretty_generate(h)` | the same, over several lines, two-space indent, `": "` after keys | | `h.to_json` | `{"sku":"LMP-01","tags":["home"]}` | `JSON.pretty_generate` is `JSON.generate` with default options `indent: " "`, `space: " "`, `object_nl: "\n"` and `array_nl: "\n"`, and you can override any of them. `to_json` is not part of core Ruby: the json library mixes it into `Hash`, `Array`, `String`, `Integer`, `Float`, `NilClass`, `TrueClass`, `FalseClass` and, as a generic fallback, `Object`, which is why, in a script that has not loaded json, calling `to_json` raises `NoMethodError`. ## How the generator treats each value 1. **Native types** (Hash, Array, String, Integer, Float, `true`, `false`, `nil`) are written directly. Symbol keys and Symbol values are written as strings. 2. **Floats that JSON cannot represent** (`NaN`, `Infinity`) raise `JSON::GeneratorError` ("NaN not allowed in JSON") unless you pass `allow_nan: true`; `JSON.dump` passes it for you. 3. **Anything else** gets its `to_json` method called with the generator's state object. If the object only has the generic `Object#to_json`, that method converts `to_s` to a JSON string. 4. **Circular structures** raise `JSON::NestingError` once nesting passes the default limit of 100. Step 3 is where custom classes go wrong. A `Time` becomes its `to_s` form, such as `"2026-09-30 12:00:00 UTC"`, not ISO 8601. A plain object becomes `"#<Product:0x000...>"`. A `Data` or `Struct` instance becomes its `to_s` text. None of these raise, so the defect usually reaches a consumer before anyone notices. ## Serializing your own class **Option 1: define `to_json`.** The conventional shape delegates to a Hash: ```ruby def to_json(*args) = to_h.to_json(*args) ``` The `*args` matters. The generator passes its **state** (indentation settings, current depth) as the argument. A method that ignores it and calls `to_h.to_json` without arguments starts a fresh, compact generation, so inside `JSON.pretty_generate` your object's part of the output comes out on one line, and the depth limit no longer tracks it. **Option 2: `JSON::Coder`.** Introduced in json 2.10 and present in the 2.18 that Ruby 4.0.7 ships, a coder takes a block that converts non-native objects to native ones, without adding methods to your classes: - `JSON::Coder.new { |obj| obj.respond_to?(:to_h) ? obj.to_h : obj }` builds a coder; - `coder.dump(value)` generates JSON, calling the block for every object that is not a native JSON type; - a coder always runs with `strict: true`, so a value the block does not convert raises `JSON::GeneratorError` instead of falling back to `to_s`. **Option 3: convert first.** Build plain hashes yourself (`products.map(&:to_h)`) and generate from those. It is explicit and easy to test. ## Keys that are not Strings JSON object keys are always strings, so the generator converts Ruby keys on the way out: | Ruby key | Written as | |---|---| | `"sku"` | `"sku"` | | `:sku` | `"sku"` | | `1` | `"1"`, through `to_s` | | `nil` | `""`, through `to_s` | A round trip therefore does not preserve key types: `{1 => "a"}` becomes `{"1" => "a"}` after `JSON.parse(JSON.generate(...))`, and a Symbol-keyed hash comes back String-keyed unless you parse with `symbolize_names: true`. With `strict: true`, a key that is not a String or Symbol raises `JSON::GeneratorError` instead of being converted. ## Make mistakes loud `JSON.generate(obj, strict: true)` refuses to call `to_s` on unsupported types and raises `JSON::GeneratorError` instead. In an export job, a raise at the first unexpected `Time` or custom object is far cheaper than a file full of `"#<Product...>"` strings. ## Catalogue export example Exporting the product catalogue back to a partner: - map each product to a Hash with the exact field names the partner expects; - format prices and timestamps explicitly (`price_cents`, `updated_at.iso8601`); - write with `JSON.pretty_generate` for humans or `JSON.generate` for machines; - turn on `strict: true` in tests so a new, unconverted attribute fails the build.

  • Why should a custom to_json accept and forward *args?
    The generator calls `to_json(state)` with its settings and current depth. Forwarding `*args` to `to_h.to_json` keeps pretty-printing, the nesting limit and options such as `strict` in effect for your object's contents; dropping them starts a fresh compact generation in the middle of the output.
  • What does JSON.generate do with a Float::NAN value?
    It raises `JSON::GeneratorError`, because `NaN` is not valid JSON. Passing `allow_nan: true` writes it as `NaN`, and `JSON.dump` does that by default, which produces output many other JSON parsers will reject.

saying these in an interview costs you the question

  • to_json is a core Ruby method available without require
  • An object without to_json makes JSON.generate raise by default
  • pretty_generate produces different data, not just different whitespace
  • A custom to_json can ignore its arguments safely
  • Time objects are written as ISO 8601 strings automatically