Rescue from SmarterJSON::Error (recommended): SmarterJSON raises only on genuinely unreadable input (an unterminated string, a mismatched bracket), with line and column in the message. Rescuing from SmarterJSON::Error lets your application handle bad input gracefully.
process vs process_one: SmarterJSON.process is the preferred call — it always returns an Array of documents, so the count is explicit and you never silently drop one. SmarterJSON.process_one is the convenience for the single-document case: it returns that one document's value directly, and warns (never raises) if the input turned out to hold more than one. Both appear below; reach for process unless you specifically want the single value.
- Read a JSON String
- Read a JSON File
- Implicit Root Object (config-style, no braces)
- Multiple Documents (NDJSON) → Array
- Streaming a Large File with a Block
- Symbolize Keys
- Duplicate Keys
- High-Precision Numbers: BigDecimal vs Float
- Lenient Input: Comments, Trailing Commas, Unquoted Keys
- Wrapper Noise Around a Payload
- Write JSON
- Write NDJSON
- Round-Trip Read and Write
require "smarter_json"
SmarterJSON.process('{"a": 1, "b": [2, 3]}') # => [{"a"=>1, "b"=>[2, 3]}] (always an Array of documents)
SmarterJSON.process_one('{"a": 1, "b": [2, 3]}') # => {"a"=>1, "b"=>[2, 3]} (the one document's value)SmarterJSON.process_file("config.json") # => an Array of documents (same return rules as process)process_file opens the file, reads it with the labeled encoding: (default "UTF-8"), and processes it.
A config file that starts with key: value and has no outer {} is read as an object:
SmarterJSON.process_one("host: localhost\nport: 5432") # => {"host"=>"localhost", "port"=>5432}Plain process reads NDJSON / JSONL / concatenated documents with no block and no special method, and always returns an Array — [] for none, [doc] for one, [d1, d2, …] for several:
SmarterJSON.process(%({"id":1}\n{"id":2}\n{"id":3})) # => [{"id"=>1}, {"id"=>2}, {"id"=>3}]
SmarterJSON.process('{"id":1}') # => [{"id"=>1}] (one document, still an Array)
SmarterJSON.process("") # => [] (zero documents)For the single-document case, process_one returns the one value directly — and warns (never raises) if there was more than one:
SmarterJSON.process_one('{"id":1}') # => {"id"=>1}
SmarterJSON.process_one("") # => nilFor input larger than memory, pass a block. Each recovered document is yielded one at a time, and the method returns the document count instead of building an Array:
SmarterJSON.process_file("events.ndjson") { |event| EventJob.perform_async(event) }A JSONL file you already have: Claude Code stores each session as a JSONL transcript — ~/.claude/projects/<project>/<session-id>.jsonl, one JSON document per line (a message, a tool call, a result, …). It reads the same way, one record at a time:
require "awesome_print" # optional — for readable nested output
SmarterJSON.process_file("#{Dir.home}/.claude/projects/<project>/<session-id>.jsonl") do |entry|
ap entry # each line is a full document
puts "-" * 80
endFilter and rewrite as a stream — SmarterJSON.foreach: foreach(source) is the composable sibling of process_file; source is a file path or any IO (a socket, a StringIO, an open File). Without a block it returns a plain Enumerator (like CSV.foreach) that reads one document at a time, so it chains with .select / .map; add .lazy to keep the whole pipeline bounded in memory. This filters a transcript down to its user/assistant turns and writes a smaller file, never loading all of it:
File.open("filtered.jsonl", "w") do |out|
SmarterJSON.foreach("session.jsonl", symbolize_keys: true)
.lazy
.select { |doc| %w[user assistant].include?(doc[:type]) }
.each { |doc| out.puts SmarterJSON.generate(doc) }
endSmarterJSON.process_one('{"a": 1, "b": 2}', symbolize_keys: true) # => {:a=>1, :b=>2}By default the last value wins. Pass :first_wins to keep the first instead (either way, the repeat is reported through on_warning):
SmarterJSON.process_one('{"a":1,"a":2}') # => {"a"=>2} (:last_wins, the default)
SmarterJSON.process_one('{"a":1,"a":2}', duplicate_key: :first_wins) # => {"a"=>1}The default :auto keeps high-precision decimals as BigDecimal (matching Oj). Force Float for raw speed when you don't need the precision:
SmarterJSON.process_one("65.613616999999977") # => BigDecimal (:auto, the default)
SmarterJSON.process_one("65.613616999999977", decimal_precision: :float) # => 65.613616999999977 (a Float)SmarterJSON.process_one(<<~JSON)
{
host: localhost, # unquoted key, quoteless value, and a trailing comma
port: 5432,
/* block comment */
url: http://example.com
}
JSON
# => {"host"=>"localhost", "port"=>5432, "url"=>"http://example.com"}A #/// only starts a comment when preceded by whitespace, so http://example.com stays a string rather than being truncated.
SmarterJSON.process_one(<<~JSON)
{
user_id: 007, # bare leading zero -> kept as a string
zip: 02139, # ditto: zip codes keep their leading zero
balance: -007.50, # a sign / decimal point / exponent makes it a number
deleted_at: NULL # SQL / R / YAML null spelling -> nil
}
JSON
# => {"user_id"=>"007", "zip"=>"02139", "balance"=>-7.5, "deleted_at"=>nil}A bare leading-zero integer is kept as a string so identifiers, zip codes, and account numbers don't lose their zeros; a sign, decimal point, or exponent marks numeric intent (-007.50 → -7.5). Null and NULL join null / None / undefined as spellings of nil; a quoted "NULL" stays a string.
SmarterJSON.process_one(<<~TEXT)
Here is the JSON:
```json
{
"a": 1
}
```
TEXT
# => {"a"=>1}SmarterJSON.process_one(<<~TEXT)
Here is the result:
{
"a": 1
}
Hope this helps.
TEXT
# => {"a"=>1}SmarterJSON.process_one("<json>{\"a\":1}</json>")
# => {"a"=>1}SmarterJSON.process(<<~TEXT)
first attempt:
{"a":1}
corrected payload:
{"b":2}
TEXT
# => [{"a"=>1}, {"b"=>2}]SmarterJSON.generate({ "a" => 1, "b" => [2, 3] }) # => '{"a":1,"b":[2,3]}'
SmarterJSON.generate([1, 2, 3]) # => '[1,2,3]'An Array writes one element per line:
SmarterJSON.generate([{ "id" => 1 }, { "id" => 2 }], format: :ndjson) # => "{\"id\":1}\n{\"id\":2}\n"obj = { "a" => 1, "b" => [2, "three", nil, true] }
SmarterJSON.process_one(SmarterJSON.generate(obj)) == obj # => truePREVIOUS: Configuration Options | UP: README