Skip to main content

Mike Kreuzer

JSON: a lowest common denominator too far

April 10, 2016

JSON, much like its ancestral home JavaScript, is both horrible and ubiquitous. So because serializing an array of structs to JSON and writing that output to a file either in compact form or pretty-printed is something I do all the time, looking at how different languages handle that task is one way to see how easy the different languages are to work with day to day, as well as perhaps giving something of an insight into their philosophies & communities.

It's neither an esoteric task or a big ask you'd have thought, but not all these languages are up to it.

The code examples here are the start of bits of code used to generate JSON for Ripley, my reddit programming languages index. Month two of Ripley's only a few weeks away.

Python

JSON comes as part of Python's standard library, and pretty printing the output's just a matter of adding some extra arguments to the one function, json.dump. There's just the one way of completing the task and it's pretty straightforward.

Python doesn't really have structs (it does have things called structs but they're packed strings) so here's the deed done with dictionaries (hashes), so the output lacks the null values output by the other languages.

import json

LANG = [{'name': 'Ada', 'url': 'https://www.reddit.com/r/ada/', 'count': 0},
        {'name': 'C', 'url': 'https://www.reddit.com/r/C_Programming', 'count': 0},]

with open('pretty.json', 'w+') as f:
    json.dump(LANG, f, sort_keys=True, indent=2)

with open('small.json', 'w+') as f:
    json.dump(LANG, f)

Python 3 also has a pprint module, and Python remains caught between versions 2 & 3 - version 2's used here, that's the version almost everyone seems to still use & while people don't have any reason to break their existing code by upgrading they won't, & the language will stagnate.

Ruby

Ruby has structs - but doesn't use named arguments when initializing them & the functions that serialize data to JSON expect hashes, so you need to convert them.

Ruby also handles JSON in its standard library though, and it's also pretty easy to write as you'll see in the following code, but as is the Ruby way - there's not just more than one way to do things, outputting compact or prettily formatted JSON requires a completely different set of functions and idioms, calling #to_json on the array, or passing the array as an argument to JSON.pretty_generate.

Luckily though there's just the one module dealing with JSON - Ruby's standard library classes exploded in number & scope when Rails took off - and have been largely left to rot now Ruby's no longer the new shiny thing, leaving behind the high water mark of date, and time, and datetime…

require 'json'

Lang = Struct.new(:name, :url, :countStr, :count)
languages = [Lang.new('Ada', 'https://www.reddit.com/r/ada/'),
             Lang.new('C', 'https://www.reddit.com/r/C_Programming')]

hash_array = languages.map { |o| Hash[o.each_pair.to_a] }
File.write 'small.json', hash_array.to_json
File.write 'pretty.json', JSON.pretty_generate(hash_array)

Elixir

Building on 30 years of Erlang's success at not coming up with anything closer to a string than a linked list of integers (seriously, that's what Erlang uses) Elixir works surprisingly well. It's slightly faster than Ruby, and offers much easier parallel code.

I wrote about my infatuation with Elixir and that generated some good comments on reddit. One of the comments allayed some of my fears about macros, they work better than I thought - a macro's used here to mark up the struct.

Elixir uses Mix to create & run a project, which works well, but the salient part of the code follows, it's small & elegant, just a few lines…

defmodule Language do
  @derive [Poison.Encoder]
  defstruct name: "-", url: "", countStr: "", count: 0
end

defmodule Lex do
  def write do
    languages = [%Language{name: "Ada", url: "https://www.reddit.com/r/ada"},
      %Language{name: "C", url: "https://www.reddit.com/r/C_Programming"},]
    json = Poison.encode! languages
    File.write! "small.json", json, [:write]
  end
end

Lex.write

Elixir relies on a 3rd party module (Poison) to deal with JSON, and that doesn't do pretty printing as far as I could see.

Elixir programers sometimes get very excited about chaining functions together with its pipe operator (yes, just like you can do in almost any language, usually with a dot), but you can't pipe to File.write! because in Elixir you pipe input to the first argument of a function and the data's the second argument… there are lots of rough edges still, but my infatuation continues.

(Edited May 2017 - to remove the pipe operator that Kramdown insists on interpreting as meaning a table… sigh.)

Go

There's a lot of boilerplate to do anything in Go, whether that's handling errors, writing to a file, or anything at all you'll be writing it yourself. It's very wordy, but it's still pretty readable and usually much faster than the interpreted languages, microseconds instead of many milliseconds. It's still slower than the really fast languages that usually take things down to nanoseconds though (C, C++, Rust, Swift…) and instead of half a dozen lines of code for Python & Ruby you'll be needing 40-something lines…

package main

import (
	"bytes"
	"encoding/json"
	"os"
)

func throw(err error) {
	if err != nil {
		panic(err)
	}
}

func writeBytes(fileName string, b []byte) {
	f, err := os.Create(fileName)
	throw(err)
	defer f.Close()
	_, err = f.Write(b)
	throw(err)
}

func main() {
	type Lang struct {
		Name     string `json:"name"`
		Url      string `json:"url"`
		CountStr string `json:"countStr"`
		Count    int    `json:"count"`
	}
	langs := []Lang{
		{"Ada", "https://www.reddit.com/r/ada/", "", 0},
		{"C", "https://www.reddit.com/r/C_Programming", "", 0},
	}
	b, err := json.Marshal(langs)
	throw(err)

	var out bytes.Buffer
	json.Indent(&out, b, "", "  ")
	writeBytes("pretty.json", out.Bytes())

	writeBytes("small.json", b)
}

For compact JSON you pass a byte array, for pretty printing you pass a buffer… it all takes extra time to write & to maintain, but it works. The way structs are marked up using back-ticks looks terrible but is quite clever.

Typed, compiled languages as you'd expect have a harder time with JSON for all the reason JSON is an awful format, there are no versions, comments, or types in JSON.

Rust

Rust is also pretty wordy, but unlike Go uses Cargo much like Elixir uses Mix to set up & run projects, and Cargo's equally good at what it does. The pattern matching construct (used here for error handling) and the macros also feel very Elixir-like to me.

While Rust handles simple JSON output well enough I couldn't work out how to get it to pretty print. The docs said it could, but not how, and I couldn't work it out. Maybe it does, the docs only seem to relate tangentially to the evolving language but that's improving I gather.

extern crate rustc_serialize;
use rustc_serialize::json;
use std::error::Error;
use std::io::prelude::*;
use std::fs::File;
use std::path::Path;

#[derive(RustcDecodable, RustcEncodable)]
pub struct LangStruct {
    name: String,
    url: String,
    countStr: String, //count_str is canonical
    count: u8,
}

fn main() {
    let obj_array: [LangStruct; 2] = [
        LangStruct{
            name: "Ada".to_string(),
            url: "https://www.reddit.com/r/ada/".to_string(),
            countStr: "0".to_string(),
            count: 0
        },
        LangStruct{
            name: "C".to_string(),
            url: "https://www.reddit.com/r/C_Programming".to_string(),
            countStr: "0".to_string(),
            count: 0
        },
    ];
    let encoded = json::encode(&obj_array).unwrap();

    let path = Path::new("small.json");
    let mut file = match File::create(&path) {
        Err(why) => panic!("{}", Error::description(&why)),
        Ok(file) => file,
    };

    match file.write_all(encoded.as_bytes()) {
        Err(why) => panic!("{}", Error::description(&why)),
        Ok(_) => println!("done"),
    };
}

Swift

Swift has great docs. Swift is great, great fun to write… but unstable a language as they come. Don't write Swift without the expectation that you'll need to rewrite it sooner rather than later.

Swift's also had a bewildering number of solutions to the JSON problem blossom on GitHub - the main thing Swift gets used to build some days seems to be JSON libraries. None work very well, none last or outgrow their rivals, it's a mess, so I'm not including any code here.

Get your act together Swift.

All in all a lot of the heat's gone out of the language wars, but here I am writing (badly I'm sure) in five languages and being snarky about a sixth. That's after being paid to write in & now avoiding at least two more… It'd be nice to be able to specialize a bit. Maybe I'll just pick one & ignore the pain. Maybe.