← Code Compare

JSON: Parse and Produce

The same round-trip in all eight languages: take the JSON text {"name":"ada","age":36}, parse it, bump age by one, and produce the JSON text back out - printing {"age":37,"name":"ada"} with keys in sorted order. Watch where JSON lives: a batteries-included standard module (Python, Go, Raku, Perl) versus an external library (Rust's serde_json, OCaml's Yojson), and - for Guji - a hand-rolled sum type rendered by match, since v0 has no JSON in its prelude. Notice too which languages give you sorted keys for free and which need an explicit nudge.

Show: GujiGoOCamlHaskellPerlRakuRustPython
Guji
# Guji has no JSON codec in its prelude, so JSON is modeled as a sum type:
# a value "is one of" these shapes (§9), rendered back by `match`.
enum Json {
    JNull
    JBool($b: Bool)
    JNum($n: Int)
    JStr($s: Str)
}

# Produce: render one Json value to compact text.
sub render($j: Json): Str {
    match $j {
        JNull     { "null" }
        JBool($b) { if $b { "true" } else { "false" } }
        JNum($n)  { "$n" }
        JStr($s)  { "\"$s\"" }
    }
}

sub main() {
    $src = '{"name":"ada","age":36}'

    # Parse: lift the two fields out with named-capture regexes (§13).
    $name = match $src ~~ /"name":"(?<v>\w+)"/ {
        Some($m) { $m<v>.unwrap_or("?") }
        None     { "?" }
    }
    $age = match $src ~~ /"age":(?<v>\d+)/ {
        Some($m) { $m<v>.unwrap_or("0").parse_int().unwrap_or(0) }
        None     { 0 }
    }

    # Build a map of typed Json values and bump the age by one.
    %doc = {"name": JStr($name), "age": JNum($age + 1)}

    # Produce: sort the keys explicitly, render each value, join.
    @parts = %doc.keys().sort().map(sub($k) {
        "\"$k\":" ~ render(%doc{$k})
    })
    print("\{" ~ @parts.join(",") ~ "}")
}

Guji's prelude (§15) ships no JSON codec, so the idiomatic move is to model JSON as an enum - the natural "is one of these shapes" sum type (§9) - and render it with an exhaustive match. Parsing leans on Guji's signature text feature: named-capture regex literals (§13) yield Option[Match], so each lookup is a match you must handle, and .parse_int() turns the captured digits into an Int. The two typed values go into a real Map[Str, Json], and producing sorted output is an explicit nudge - %doc.keys().sort() - mirroring the dict-based languages here. Note "\{" escapes the brace so it is a literal, not the start of a { … } interpolation (§12). Guji is a compiled, statically typed language: this runs identically on the reference interpreter (guji file.guji) and as a native binary (guji build).

Go
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	src := `{"name":"ada","age":36}`

	// Parse into a generic map of string -> any.
	var doc map[string]any
	if err := json.Unmarshal([]byte(src), &doc); err != nil {
		panic(err)
	}

	// JSON numbers decode as float64; bump age by one.
	doc["age"] = doc["age"].(float64) + 1

	// Produce: Marshal sorts map keys alphabetically.
	out, _ := json.Marshal(doc)
	fmt.Println(string(out))
}

Go's encoding/json is in the standard library. Unmarshalling into a map[string]any gives a generic tree, but every JSON number arrives as a float64, so reading age needs a type assertion .(float64) before the increment. json.Marshal emits a compact object and - conveniently for this task - sorts string map keys alphabetically by default, so {"age":37,"name":"ada"} falls out without any extra sorting step. Decoding into a typed struct instead would skip the assertions but fix the shape ahead of time.

OCaml
(* Uses the Yojson library: opam install yojson *)
let () =
  let src = {|{"name":"ada","age":36}|} in

  (* Parse into Yojson's variant tree. *)
  let doc = Yojson.Safe.from_string src in
  let bumped =
    match doc with
    | `Assoc fields ->
        let fields =
          List.map
            (function
              | "age", `Int n -> ("age", `Int (n + 1))
              | kv -> kv)
            fields
        in
        (* Produce: sort the keys, then serialize. *)
        `Assoc (List.sort (fun (a, _) (b, _) -> compare a b) fields)
    | other -> other
  in
  print_endline (Yojson.Safe.to_string bumped)

OCaml has no JSON in its standard library, so the idiomatic choice is the Yojson library, whose Yojson.Safe.t is a polymorphic-variant tree - `Assoc for objects, `Int, `String, and so on. Parsing and matching are one and the same: match doc with \Assoc fieldsdestructures the object as an association list, and a nested pattern rewrites theageentry. Yojson preserves insertion order, so producing sorted output is an explicitList.sorton the pairs beforeto_string`.

Haskell
-- Uses the aeson library: cabal install aeson
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson (Value (Number, Object), decode, encode)
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString.Lazy.Char8 as B

main :: IO ()
main = do
  let src = "{\"name\":\"ada\",\"age\":36}"
  -- Parse into aeson's Value tree (Maybe handles failure).
  case decode src :: Maybe Value of
    Just (Object o) -> do
      -- Bump age by one, then re-encode (aeson sorts object keys).
      let bumped = KM.adjust inc "age" o
      B.putStrLn (encode (Object bumped))
    _ -> putStrLn "parse error"
  where
    inc (Number n) = Number (n + 1)
    inc v = v

Haskell has no JSON in base, so the de-facto choice is the aeson library, whose Value is an algebraic data type with Object, Number, String, etc. decode returns a Maybe Value, so failure is handled by pattern-matching rather than exceptions, and KeyMap.adjust rewrites the age field functionally. Aeson's encode already emits object keys in sorted order, so {"age":37,"name":"ada"} falls out with no extra step; for a known schema you would derive FromJSON/ToJSON on a record instead of poking at a Value.

Perl
use strict;
use warnings;
use JSON::PP;

my $src = '{"name":"ada","age":36}';

# Parse: decode the JSON text into a Perl hash ref.
my $doc = decode_json($src);
$doc->{age} += 1;

# Produce: ->canonical sorts the keys for deterministic output.
my $json = JSON::PP->new->canonical;
print $json->encode($doc), "\n";

JSON::PP ships with Perl, so no install is needed; decode_json turns JSON text into ordinary Perl data (a hash ref here), and Perl's automatic numeric context lets += 1 increment age with no casting. The default encode_json would emit keys in hash order, which is randomized, so the object form JSON::PP->new->canonical is used to sort keys for a stable result. The faster XS-backed JSON::XS (or the Cpanel::JSON::XS it descends from) shares this exact API.

Raku
use JSON::Fast;

my $src = '{"name":"ada","age":36}';

# Parse: from-json returns a Raku Hash of native values.
my %doc = from-json($src);
%doc<age> += 1;

# Produce: :sorted-keys orders the keys, :!pretty keeps it on one line.
say to-json(%doc, :sorted-keys, :!pretty);

Raku reaches for the JSON::Fast module (the de-facto standard, a fast drop-in for JSON::Tiny): from-json parses JSON text into native Raku values - here a Hash - and %doc<age> += 1 increments in place. By default to-json pretty-prints over several lines, so :!pretty collapses it to one line, and :sorted-keys orders the keys for a deterministic {"age":37,"name":"ada"}. The whole round-trip is two library calls plus a single field update.

Rust
// Uses the serde_json crate: cargo add serde_json
use serde_json::{json, Value};

fn main() {
    let src = r#"{"name":"ada","age":36}"#;

    // Parse into an untyped serde_json Value tree.
    let mut doc: Value = serde_json::from_str(src).unwrap();

    // Bump age; as_i64 reads the number, then overwrite the field.
    let age = doc["age"].as_i64().unwrap();
    doc["age"] = json!(age + 1);

    // Produce: serde_json's default Map is a BTreeMap, so keys sort.
    println!("{}", serde_json::to_string(&doc).unwrap());
}

Rust's JSON lives in the external serde_json crate. from_str parses into an untyped Value enum; indexing doc["age"] and .as_i64() reads the number as an Option (here .unwrap()'d), and the json! macro builds the replacement value. By default serde_json backs objects with a BTreeMap, so serialization sorts keys automatically - enabling the preserve_order feature would instead keep input order. For a known schema you would #[derive(Serialize, Deserialize)] a struct rather than poke at a Value.

Python
import json

src = '{"name":"ada","age":36}'

# Parse: a JSON string becomes a dict of native Python values.
doc = json.loads(src)
doc["age"] += 1

# Produce: sort_keys + compact separators give deterministic output.
print(json.dumps(doc, separators=(",", ":"), sort_keys=True))

Python's json module is standard and effortless: json.loads turns the text into a dict of native values, and doc["age"] += 1 increments the integer directly. json.dumps serializes back, where sort_keys=True orders the keys and separators=(",", ":") strips the default spaces for compact output. This three-line round-trip is the baseline the library-based languages here are measured against.