JSON · for Gleam

gleamson

A pure-Gleam JSON library. A transparent value tree, combinator decoders that accumulate every error, and the very same behaviour on Erlang and JavaScript.

$ gleam add gleamson
cat.gleam
import gleamson
import gleamson/decode

pub type Cat {
  Cat(name: String, lives: Int)
}

let cat = {
  use name <- decode.field("name", decode.string)
  use lives <- decode.field("lives", decode.int)
  decode.success(Cat(name:, lives:))
}

decode.from_string("{\"name\":\"Nono\",\"lives\":9}", cat)
// -> Ok(Cat("Nono", 9))
Why gleamson

Built the way Gleam wants it.

No FFI, no surprises. Just data you can see, decoders you can compose, and errors that tell you everything that went wrong.

Pure Gleam

One codebase that behaves identically on Erlang and JavaScript, with no Erlang/OTP version requirement.

Transparent values

Json is an ordinary type you can pattern‑match, walk and build. Parse → encode round‑trips faithfully.

Errors accumulate

Decoders report every problem at once — each with a path like ["lives"] — not just the first.

Pretty & precise

to_string_pretty for humans, compact to_string for the wire, and parse errors with exact byte positions.

Compose decoders

field, list, dict, optional, one_of, then, enum — the familiar use style.

Merge, Pointer & Patch

RFC 7386 merge, RFC 6901 JSON Pointer, and RFC 6902 JSON Patch with apply and diff — built in.

In practice

Three small examples.

import gleamson.{Int, Null, Object, String}

Object([
  #("name", String("Lucy")),
  #("lives", Int(9)),
  #("flaws", Null),
  #("nicknames", gleamson.array(["Boo", "Bug"], of: String)),
])
|> gleamson.to_string
// -> {"name":"Lucy","lives":9,"flaws":null,"nicknames":["Boo","Bug"]}
// Both fields are the wrong type — you get BOTH errors, with paths.
decode.from_string("{\"name\": 42, \"lives\": \"nine\"}", cat)
// -> Error(CouldNotDecode([
//      DecodeError("String", "Int", ["name"]),
//      DecodeError("Int", "String", ["lives"]),
//    ]))

// Want just the first instead? Use run_first.
decode.run_first(value, cat)
// -> Error(DecodeError("String", "Int", ["name"]))
import gleamson/patch.{Add, Replace}

let assert Ok(doc) = gleamson.parse("{\"a\":1,\"b\":[10]}")

// apply — atomic: all ops succeed, or none are applied
patch.apply(doc, [Replace("/a", Int(2)), Add("/b/-", Int(20))])
// -> Ok({"a":2,"b":[10,20]})

// diff two documents into a patch you can send over the wire
let ops = patch.diff(from: doc, to: updated)
Ready when you are

Honest JSON, start to finish.

Transparent values, accumulating errors, and one behaviour across every target. Add it in one line.

$ gleam add gleamson