← Code Compare

JSON

One round-trip across all five BEAM languages: parse {"name":"ada","age":36}, bump age to 37, and serialise it back to a JSON string. Watch who ships JSON in the box versus who reaches for a library - modern Erlang/OTP 27+ has a built-in json module and Elixir leans on the ubiquitous Jason (or its own JSON since 1.18), Gleam uses gleam_json with a typed decoder, LFE just calls Erlang's json, and Luerl's plain Lua has no JSON library at all, so the idiom is to hand-roll a tiny encoder. Notice the recurring BEAM theme: object keys decode to binaries (Erlang/Elixir/LFE), Gleam makes you declare the shape up front and returns a Result, and only Lua mutates the table in place.

Show: ErlangElixirGleamLFELuerl
Erlang
-module(bump).
-export([run/0]).

run() ->
    Input = <<"{\"name\":\"ada\",\"age\":36}">>,
    %% OTP 27+ ships a built-in json module; keys decode to binaries.
    Decoded = json:decode(Input),
    #{<<"age">> := Age} = Decoded,
    Updated = Decoded#{<<"age">> => Age + 1},
    %% json:encode/1 returns an iolist; flatten it to a binary.
    Out = iolist_to_binary(json:encode(Updated)),
    io:format("~s~n", [Out]),
    Out.
%% => {"name":"ada","age":37}

Since OTP 27 Erlang has a built-in json module: json:decode/1 turns the binary into a map with binary keys, the #{... => ...} syntax returns an updated copy, and json:encode/1 produces an iolist you flatten with iolist_to_binary/1. Pre-27 code reaches for jsx instead.

Elixir
input = ~s({"name":"ada","age":36})

# Jason is the de-facto JSON library (Elixir 1.18+ also has a built-in JSON).
{:ok, data} = Jason.decode(input)

updated = Map.update!(data, "age", &(&1 + 1))

updated
|> Jason.encode!()
|> IO.puts()
# => {"name":"ada","age":37}

Jason.decode/1 returns a tagged {:ok, map} with string keys, Map.update!/3 bumps age with the &(&1 + 1) capture, and the result pipes through Jason.encode!/1 (the ! variant raises instead of returning a tuple) to IO.puts.

Gleam
import gleam/io
import gleam/json
import gleam/dynamic/decode

pub type Person {
  Person(name: String, age: Int)
}

fn person_decoder() -> decode.Decoder(Person) {
  use name <- decode.field("name", decode.string)
  use age <- decode.field("age", decode.int)
  decode.success(Person(name:, age:))
}

pub fn main() {
  let input = "{\"name\":\"ada\",\"age\":36}"

  let assert Ok(person) =
    json.parse(from: input, using: person_decoder())

  let updated = Person(..person, age: person.age + 1)

  let out =
    json.object([
      #("name", json.string(updated.name)),
      #("age", json.int(updated.age)),
    ])
    |> json.to_string

  io.println(out)
  // => {"name":"ada","age":37}
}

Gleam makes the shape explicit: a decode.Decoder declares each field's type, json.parse returns a Result you must handle (here let assert Ok), and you rebuild the JSON from json.object/json.string/json.int - there is no untyped map and no null.

LFE
(defmodule bump
  (export (run 0)))

(defun run ()
  (let* ((input #"{\"name\":\"ada\",\"age\":36}")
         ;; Same OTP 27+ json module as Erlang; keys are binaries.
         (decoded (json:decode input))
         (age (maps:get #"age" decoded))
         (updated (maps:put #"age" (+ age 1) decoded))
         (out (iolist_to_binary (json:encode updated))))
    (io:format "~s~n" (list out))
    out))
;; (bump:run) => {"name":"ada","age":37}

LFE shares Erlang's runtime, so it calls the identical json:decode/json:encode; #"..." is the binary literal, maps:get/maps:put read and update the decoded map (binary keys again), and let* threads each step in Lisp form.

Luerl
-- Standard Lua (what Luerl runs) ships NO json library, so we hand-roll.
local function encode(t)
  return string.format('{"name":%q,"age":%d}', t.name, t.age)
end

-- For this fixed shape, "parsing" is a literal table; real code would
-- use a json.lua module. Lua tables are MUTABLE, so we edit in place.
local person = { name = "ada", age = 36 }

person.age = person.age + 1

print(encode(person))
-- => {"name":"ada","age":37}

Plain Lua has no JSON in its standard library, so the idiom is a tiny hand-rolled encoder (string.format with %q for a quoted string and %d for the number); unlike the immutable BEAM languages, the Lua table is mutated in place with person.age = person.age + 1.