gleamson ✨
A pure-Gleam JSON library: a transparent value tree, a single-pass parser, and combinator decoders. No FFI, no platform JSON dependency, identical behaviour on Erlang and JavaScript.
gleam add gleamson
Why another JSON library?
gleamson is written entirely in Gleam instead of delegating to the runtime’s
native JSON facilities. The trade that buys you:
- No Erlang/OTP version requirement. Works wherever Gleam works.
- The same behaviour on both targets, down to the error positions.
- Precise, positioned parse errors on every runtime — no scraping of browser error strings.
- A transparent
Jsontype you can pattern match on, walk, and build directly. JSON is just data here, not an opaque handle.
Honest note on speed: parsing leans on Gleam’s bit-array pattern matching, which
is fast on the BEAM and allocation-light. For very large payloads on the
JavaScript target, a runtime’s native JSON.parse (written in C++) will still
win on raw throughput. If you need that, parse natively and feed the result into
a decode.Decoder; the decoder layer doesn’t care where the value came from.
Encoding
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"]}
Because Json is a transparent type, encoding is just building a value with its
constructors. The helpers array, nullable, and from_dict cover the common
shapes.
Parsing into a value
import gleamson
let assert Ok(value) = gleamson.parse("{\"user\":{\"name\":\"Ada\"}}")
gleamson.get(value, at: ["user", "name"])
// -> Ok(String("Ada"))
field, get, index, to_dict, and the as_* helpers let you walk a value
without ceremony. Object entries keep their order and duplicates, so
parse |> to_string round-trips faithfully.
Decoding into your own types
import gleamson
import gleamson/decode
pub type Cat {
Cat(name: String, lives: Int, nicknames: List(String))
}
pub fn cat_from_json(text: String) -> Result(Cat, decode.Error) {
let cat = {
use name <- decode.field("name", decode.string)
use lives <- decode.field("lives", decode.int)
use nicknames <- decode.field("nicknames", decode.list(decode.string))
decode.success(Cat(name:, lives:, nicknames:))
}
decode.from_string(text, cat)
}
A Decoder(t) is simply fn(Json) -> #(t, List(DecodeError)), so writing a
custom one is just writing a function.
Errors accumulate. When several fields are wrong, you get every error in one go rather than stopping at the first:
// {"name": 42, "lives": "nine"} ->
// Error(CouldNotDecode([
// DecodeError("String", "Int", ["name"]),
// DecodeError("Int", "String", ["lives"]),
// ]))
Each error carries a path (e.g. ["lives"], or ["items", "2", "id"] for
nested structures) pointing straight at the offending value.
Two runners let you choose how much you want back:
run(json, decoder) -> Result(t, List(DecodeError))— every error.run_first(json, decoder) -> Result(t, DecodeError)— just the first.from_string(text, decoder) -> Result(t, Error)— parse + decode, with all decode errors wrapped inCouldNotDecode.
Combinators: field, optional_field, at, list, dict, optional, map,
success, failure, and the primitives string / int / float / bool /
json.
Layout
src/gleamson.gleam -- Json type, parser, encoder, value helpers
src/gleamson/decode.gleam -- combinator decoders over Json
test/gleamson_test.gleam -- examples that double as a test suite
License
Apache-2.0
More utilities
Pretty printing — to_string_pretty(json) (2 spaces) or
to_string_pretty_with(json, spaces: 4) for human-readable, indented output.
Merging — merge(into:, patch:) applies a JSON Merge Patch (RFC 7386):
objects merge recursively, a Null deletes a key, anything else replaces.
Useful for layering config or applying partial updates.
Structural equality — semantically_equal(a, b) compares values while
ignoring object key order (arrays stay ordered). Handy in tests.
Extra decoders — alongside field / list / dict / optional:
one_of(first, [others])— try decoders in turn, first success wins.then(decoder, apply:)— decode, then choose the next decoder; great for validation or discriminated unions keyed on a"type"field.index(at:, of:)— decode a single array element by position.
Enum decoding — enum(first, or: [...]) maps JSON strings to your own
type’s variants: enum(#("buy", Buy), or: [#("sell", Sell)]).
JSON Pointer (RFC 6901) — pointer(value, "/a/items/0/id") looks up a
value by path string; "" returns the whole document, and keys with / or ~
use the ~1 / ~0 escapes.
JSON Patch (RFC 6902)
The gleamson/patch module applies and computes patches.
import gleamson
import gleamson/patch.{Add, Replace}
let assert Ok(doc) = gleamson.parse("{\"a\":1,\"b\":[10]}")
// apply (atomic: all ops succeed, or none are applied)
let assert Ok(out) =
patch.apply(doc, [Replace("/a", gleamson.Int(2)), Add("/b/-", gleamson.Int(20))])
// diff two documents into a patch
let ops = patch.diff(from: doc, to: out)
// patches are JSON too
patch.to_json(ops) // -> a Json array
decode.run(some_json, patch.decoder()) // -> Result(List(Operation), _)
Operations: Add, Remove, Replace, Move, Copy, Test (paths are JSON
Pointers). diff is correct but not minimal — array edits are positional, with
no move detection.