Avoiding extra lines in Ruby JSON.pretty_generate

Viewed 536

I would like to produce JSON formatted like this:

{
   "a": {
       "b": 1
   },
   "c": [
       2
   ],
   "d": [],
   "e": {}
}

This is easy to do with the standard JSON support in Python, JavaScript, and Go.

In Ruby, I'm having trouble getting that exact output:

$ ~/.rbenv/versions/2.6.3/bin/irb 
irb(main):001:0> require 'json'
=> true
irb(main):002:0> puts JSON.pretty_generate({"a" => {"b" => 1}, "c" => [2], "d" => [], "e" => {}})
{
  "a": {
    "b": 1
  },
  "c": [
    2
  ],
  "d": [

  ],
  "e": {
  }
}
=> nil

The problems:

  • Empty objects are rendered on two lines.
  • Empty arrays are rendered on three(!) lines.

Is there a simple way to get the Ruby standard JSON support to render the output I want?

4 Answers

I'm not sure if it's possible though the options, but here is a regex that achieves what you want.

structure = {
  "a" => {"b" => 1},
  "c" => [2],
  "d" => [],
  "e" => {}, 
  "f" => "string with [     ] and {       }"
}
json = JSON.pretty_generate(structure)

regex = /(?<content>"(?:[^\\"]|\\.)+")|(?<open>\{)\s+(?<close>\})|(?<open>\[)\s+(?<close>\])/m
puts json.gsub(regex, '\k<open>\k<content>\k<close>')
# {
#   "a": {
#     "b": 1
#   },
#   "c": [
#     2
#   ],
#   "d": [],
#   "e": {},
#   "f": "string with [     ] and {       }"
# }
#=> nil

The regex used starts matching from either a ", [ or { character.

If it starts matching from " it will match till the end of the string, ignoring everything in-between. Capturing the whole string in the content group. The open and close groups will be empty. While substituting with gsub the whole content group will be placed back. This is done to negate { and [ characters in string context.

If it starts matching from a [ or { it only allows whitespace characters and looks for the matching ] or }. The opening and closing characters will be placed in the open and close groups respectively, the whitespace in-between is not captured. The content group will be empty. Only the open and close groups will be placed back while substituting (removing the whitespace characters).

Here's a diagram of the match. It didn't like named groups, so I removed them to render the diagram. group #1 correspond with content, group #2 and group #4 correspond with open, group #3 and group #5 correspond with close.

regex diagram

It's not possible using Ruby's standard library JSON. JSON.pretty_generate uses JSON.generate with a pre-defined configuration:

JSON::PRETTY_STATE_PROTOTYPE.to_hash
{:indent=>"  ",
 :space=>" ",
 :space_before=>"",
 :object_nl=>"\n",
 :array_nl=>"\n",
 :allow_nan=>false,
 :ascii_only=>false,
 :max_nesting=>100,
 :depth=>0,
 :buffer_initial_length=>1024}

You could try:

puts JSON.pretty_generate(object, array_nl: '')
{
  "a": {
    "b": 1
  },
  "c": [    2  ],
  "d": [  ],
  "e": {
  }
}

I know it's not exactly what you want, but using JSON.generate, you can customize JSON output generation only that much. There is no separate configuration for empty objects/arrays. Also, it's implemented in C, so monkey patching won't work as well.

āœ… I found the neatjson gem useful to generate JSON where empty arrays/hashes are rendered on a single line instead of 3.

You can use this online visual tool to find the ad-hoc options.

You can install it using either gem install neatjson or append gem 'neatjson', '0.9' to your Gemfile(0.9 was the most recent version at the time of this writing, feel free to adjust).

Then here is how to use it to get the expected output:

require 'json'
require 'neatjson'

structure = {
  "a" => {"b" => 1},
  "c" => [2],
  "d" => [],
  "e" => {}, 
  "f" => "string with [     ] and {       }"
}

json = JSON.neat_generate(
  structure,
  { 
    wrap:        true,
    indent:      '    ',
    after_colon: 1,
    after_comma: 1
  }
)
puts "#{json}\n"

I hope this helps, I did not find a way to configure JSON.pretty_generate do this and neatjson can fix this issue. It offers a myriad of options to fine tune the JSON string.

Since Ruby's JSON formatting is limited, I generally use jq to format JSON strings both in terminal and Ruby. When using jq with a pipe, the identity filter '.' is optional, but I like to keep it anyway.

require 'json'
hash = {"a"=>{"b"=>1}, "c"=>[2], "d"=>[], "e"=>{}}
puts `echo '#{hash.to_json}' | jq '.'`

prints:

{
  "a": {
    "b": 1
  },
  "c": [
    2
  ],
  "d": [],
  "e": {}
}
Related