# 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.
```sh
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 `Json` type** 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
```gleam
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
```gleam
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
```gleam
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:
```gleam
// {"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 in `CouldNotDecode`.
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.
```gleam
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.