melange-json
April 28, 2026 · View on GitHub
Compositional JSON encode/decode library and PPX for Melange.
Based on @glennsl/bs-json.
The Decode module in particular provides a basic set of decoder functions to be
composed into more complex decoders. A decoder is a function that takes a
Js.Json.t and either returns a value of the desired type if successful or
raises an Of_json_error exception if not. Other functions accept a decoder and
produce another decoder. Like array, which when given a decoder for type t
will return a decoder that tries to produce a value of type t array. So to
decode an int array you combine Melange_json.Of_json.int with Melange_json.Of_json.array
into Melange_json.Of_json.(array int). An array of arrays of ints? Melange_json.Of_json.(array (array int)). Dict containing arrays of ints? Melange_json.Of_json.(dict (array int)).
Example
type line = {
start: point,
end_: point,
thickness: option(int)
}
and point = {
x: int,
y: int
};
module Decode = {
let point = json =>
Melange_json.Of_json.{
x: json |> field("x", int),
y: json |> field("y", int)
};
let line = json =>
Melange_json.Of_json.{
start: json |> field("start", point),
end_: json |> field("end", point),
thickness: json |> try_or_none(field("thickness", int))
};
};
let data = {| {
"start": { "x": 1, "y": -4 },
"end": { "x": 5, "y": 8 }
} |};
let line = data |> Melange_json.of_string
|> Decode.line;
NOTE: Melange_json.Of_json.{ ... } creates an ordinary record, but also opens the
Melange_json.Of_json module locally, within the scope delimited by the curly braces, so
we don't have to qualify the functions we use from it, like field, int and
try_or_none here. You can also use Melange_json.Of_json.( ... ) to open the module
locally within the parentheses, if you're not creating a record.
See examples for more.
Installation
Install opam package manager.
Then:
opam install melange-json
Setup
Add melange-json to the libraries field in your dune file:
; ...
(libraries melange-json)
; ...
Documentation
API
For the moment, please see the interface files:
Writing custom decoders and encoders
If you look at the type signature of Melange_json.Decode.array, for example, you'll
see it takes an 'a decoder and returns an 'a array decoder. 'a decoder is
just an alias for Js.Json.t -> 'a, so if we expand the type signature of
array we'll get (Js.Json.t -> 'a) -> Js.Json.t -> 'a array. We can now see
that it is a function that takes a decoder and returns a function, itself a
decoder. Applying the int decoder to array will give us an int array decoder, a function Js.Json.t -> int array.
If you've written a function that takes just Js.Json.t and returns
user-defined types of your own, you've already been writing composable decoders!
Let's look at Decode.point from the example above:
let point = json => {
open! Melange_json.Decode;
{
x: json |> field("x", int),
y: json |> field("y", int)
};
};
This is a function Js.Json.t -> point, or a point decoder. So if we'd like
to decode an array of points, we can just pass it to Melange_json.Of_json.array to get
a point array decoder in return.
Builders
To write a decoder builder like Melange_json.Of_json.array we need to take another
decoder as an argument, and thanks to currying we just need to apply it where
we'd otherwise use a fixed decoder. Say we want to be able to decode both int points and float points. First we'd have to parameterize the type:
type point('a) = {
x: 'a,
y: 'a
}
Then we can change our point function from above to take and use a decoder
argument:
let point = (decodeNumber, json) => {
open! Melange_json.Decode;
{
x: json |> field("x", decodeNumber),
y: json |> field("y", decodeNumber)
};
};
And if we wish we can now create aliases for each variant:
let intPoint = point(Melange_json.Of_json.int);
let floatPoint = point(Melange_json.Of_json.float);
Encoders
Encoders work exactly the same way, just in reverse. 'a encoder is just an
alias for 'a -> Js.Json.t, and this also transfers to composition: 'a encoder -> 'a array encoder expands to ('a -> Js.Json.t) -> 'a array -> Js.Json.t.
PPX for Melange
A ppx deriver plugin is provided to automatically convert Melange values to and from JSON.
Installation
The PPX is included in the melange-json package. To use it, just add the
dune configuration to your project:
(library
(modes melange)
(preprocess (pps melange-json.ppx)))
Usage
To generate JSON converters for a type, add the [@@deriving json] attribute to
the type declaration, ensuring the converters for primitives like int and
string are in scope if necessary:
open Melange_json.Primitives
type t = {
a: int;
b: string;
} [@@deriving json]
This will generate the following pair of functions:
val of_json : Js.Json.t -> t
val to_json : t -> Js.Json.t
Primitives semantics
The following table summarizes the correspondence between OCaml types and JSON values for the primitives:
| OCaml type | JSON value | Sample JSON value |
|---|---|---|
int, float | Number | 1.23 |
int64 | String | "1234567890" |
bool | Boolean | true |
string | String | "foo" |
list, array | Array | [1, 2, 3] |
'a option | Null or 'a | string option is null or "foo"; int option is null or 1 |
unit | Null | null |
('a, 'b) result | Array | (int, string) result is ["Ok", 1] or ["Error", "error"] |
Generating JSON converters from type expressions
You can also generate JSON converters for a type expression using the to_json
and of_json extension points:
let json = [%to_json: int * string] (42, "foo")
[@json.default E]: default values for records
You can specify default values for record fields using the [@json.default E]
attribute:
type t = {
a: int;
b: string [@json.default "-"];
} [@@deriving of_json]
let t = of_json (Melange_json.of_string {|{"a": 42}|})
(* t = { a = 42; b = "-"; } *)
[@json.allow_extra_fields] on records
Sometimes, the JSON objects might contain keys that are not part of the OCaml
type definition. The [@json.allow_extra_fields] attribute allows you to
gracefully ignore such additional fields instead of raising an error during
deserialization.
This attribute can be used on records, even when they are embedded in other types.
Note: For the Melange PPX, ignoring extra fields is the default behavior - you don't need to explicitly add the
[@json.allow_extra_fields]attribute. The attribute is primarily useful for the native PPX where strict field checking is the default.
Example 1: Ignoring extra fields in records
type allow_extra_fields = {
a: int;
} [@@deriving json] [@@json.allow_extra_fields]
let t = allow_extra_fields_of_json (Json.parseOrRaise {|{"a": 42, "extra": "ignore me"}|})
(* t = { a = 42 } *)
The additional key "extra" in the JSON input is ignored, and the record is
successfully deserialized.
Example 2: Ignoring extra fields in inline records
type allow_extra_fields2 =
| A of { a: int } [@json.allow_extra_fields]
[@@deriving json]
let t = allow_extra_fields2_of_json (Json.parseOrRaise {|{"tag": "A", "a": 42, "extra": "ignore me"}|})
(* t = A { a = 42 } *)
In this case, the [@json.allow_extra_fields] attribute is applied directly to
the inline record in the variant constructor. This allows the variant to ignore
extra fields in the JSON payload while properly deserializing the fields that
match the type definition.
[@json.option]: a shortcut for [@json.default None]
When a field has type _ option then you can use the [@json.option] attribute
to specify that the default value is None:
type t = {
a: int;
b: string option [@json.option];
} [@@deriving of_json]
let t = of_json (Melange_json.of_string {|{"a": 42}|})
(* t = { a = 42; b = None; } *)
[@json.drop_default]: drop default values from JSON
When a field has either [@json.option] or [@json.default] attributes, you can use the [@json.drop_default]
attribute to make the generated to_json function drop the field
from the JSON output when its value matches the default.
In its flag form (no argument), [@json.drop_default] checks for None when used with
[@json.option], and requires an equal_<type> function in scope when used with
[@json.default]:
let equal_string = String.equal
type t = {
a: int;
b: string option [@json.option] [@json.drop_default];
c: string [@json.default "-"] [@json.drop_default];
} [@@deriving to_json]
let t = to_json { a = 1; b = None; c = "-"; }
(* {"a": 1} *)
For parameterized types, the equal function takes the inner type's equal_<type>
function as an argument, so a field of type int list generates a call to equal_list equal_int,
int list list generates equal_list (equal_list equal_int), and so on.
let equal_int = Int.equal
let rec equal_list equal_a a b =
match a, b with
| [], [] -> true
| x :: xs, y :: ys -> equal_a x y && equal_list equal_a xs ys
| _ -> false
type t = {
items: int list [@json.default []] [@json.drop_default];
} [@@deriving to_json]
let json = to_json { items = [] }
(* {} *)
You can also provide a custom comparison function of type 'a -> 'a -> bool directly:
type t = {
f: float [@json.default 0.0] [@json.drop_default Float.equal];
} [@@deriving to_json]
let json = to_json { f = 0.0 }
(* {} *)
[@json.drop_default_if_json_equal]: drop defaults by comparing JSON output
A (mutually exclusive) alternative to [@json.drop_default] that compares values at the JSON level
rather than requiring an equal_<type> function. This is useful for complex or
nested types where you already have to_json but don't want to derive or write
equality functions:
type color = { r: int; g: int; b: int } [@@deriving json]
type style = {
font_size: int;
background: color
[@json.default { r = 255; g = 255; b = 255 }]
[@json.drop_default_if_json_equal];
} [@@deriving json]
let json = to_json { font_size = 12; background = { r = 255; g = 255; b = 255 } }
(* {"font_size": 12} *)
[@json.key "S"]: customizing keys for record fields
You can specify custom keys for record fields using the [@json.key E]
attribute:
type t = {
a: int [@json.key "A"];
b: string [@json.key "B"];
} [@@deriving of_json]
let t = of_json (Melange_json.of_string {|{"A": 42, "B": "foo"}|})
(* t = { a = 42; b = "foo"; } *)
[@json.name "S"]: customizing the representation of a variant case
You can specify custom representation for a variant case using the [@json.name E] attribute:
type t = A | B [@json.name "bbb"] [@@deriving json]
let json = to_json B
(* "bbb" *)
[@@json.compact_variants]: compact encoding for variants and polyvariants
The [@@json.compact_variants] attribute changes the JSON encoding of variant
and polyvariant types to a compact form:
- Constructors without arguments are encoded as plain JSON strings.
- Constructors with arguments are encoded as JSON arrays
["ConstructorName", arg1, ...].
type t = A | B of int | C of int * string [@@deriving json] [@@json.compact_variants]
let json_a = to_json A
(* "A" *)
let json_b = to_json (B 42)
(* ["B", 42] *)
let json_c = to_json (C (1, "x"))
(* ["C", 1, "x"] *)
This also works for polyvariant types:
type t = [`A | `B of int] [@@deriving json] [@@json.compact_variants]
[@@deriving json_string]: a shortcut for JSON string conversion
For convenience, one can use [@@deriving json_string] to generate converters
directly to and from JSON strings:
type t = A [@@deriving json, json_string]
let "\"A\"" = to_json_string A
let A = of_json_string "\"A\""
Similarly, there's [@@deriving to_json_string] and [@@deriving of_json_string] to generate the converters separately.
PPX for OCaml native
A similar PPX is exposed in the melange-json-native package, which works with
the yojson JSON representation instead of Js.Json.t.
Installation
The PPX is included in melange-json-native package, so that package will have
to be installed first:
opam install melange-json-native
To use it, add the dune configuration to your project:
(executable
...
(preprocess (pps melange-json-native.ppx)))
Usage
From the usage perspective, the PPX is similar to the Melange one:
type t = {
a: int;
b: string;
} [@@deriving json]
This will generate the following pair of functions:
val of_json : Yojson.Basic.json -> t
val to_json : t -> Yojson.Basic.json
Refer to the PPX for Melange section for more details on usage patterns.
License
This work is dual-licensed under LGPL 3.0 and MPL 2.0. You can choose between one of them if you use this work.
Please see LICENSE.LGPL-3.0 and LICENSE.MPL-2.0 for the full text of each license.
SPDX-License-Identifier: LGPL-3.0 OR MPL-2.0