melange-json

July 6, 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 typeJSON valueSample JSON value
int, floatNumber1.23
int64String"1234567890"
boolBooleantrue
stringString"foo"
list, arrayArray[1, 2, 3]
'a optionNull or 'astring option is null or "foo"; int option is null or 1
unitNullnull
('a, 'b) resultArray(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")

Labeled tuples (OCaml 5.4+) are supported as well, and serialize to JSON objects keyed by each label:

let json = [%to_json: x:int * y:int] (~x:1, ~y:2)
(* {"x": 1, "y": 2} *)

let (~x, ~y) =
  [%of_json: x:int * y:int] (Melange_json.of_string {|{"x": 1, "y": 2}|})
(* x = 1, y = 2 *)

Members without a label are keyed by their position:

let json = [%to_json: x:int * string] (~x:1, "foo")
(* {"x": 1, "1": "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 = "-"; } *)

Extra fields on records

By default, both the native and Melange PPXs ignore JSON object keys that are not part of the OCaml record type.

type t = {
  a: int;
} [@@deriving json]

let t = t_of_json (Melange_json.of_string {|{"a": 42, "extra": "ignore me"}|})
(* t = { a = 42 } *)

[@json.allow_extra_fields] is still accepted for backwards compatibility, but is no longer necessary.

Use [@json.disallow_extra_fields] to reject unknown keys and keep strict field checking. This attribute can be used on regular records and on inline records in variant constructors.

type strict = {
  a: int;
} [@@deriving json] [@@json.disallow_extra_fields]

let _ = strict_of_json (Melange_json.of_string {|{"a": 42, "extra": "fail"}|})
(* raises: did not expect field "extra" *)
type strict_inline =
  | A of { a: int } [@json.disallow_extra_fields]
  [@@deriving json]

let _ =
  strict_inline_of_json
    (Melange_json.of_string {|["A", {"a": 42, "extra": "fail"}]|})
(* raises: did not expect field "extra" *)

[@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]

[@json.catch_all]: catch-all constructor for unknown string tags

The [@json.catch_all] attribute marks a constructor as the catch-all for any unrecognised string tag. The constructor's argument is the library type Melange_json.unknown_variant_case, a record with fields tag : string and payload : Melange_json.t list option. The decoder routes bare unknown strings and unknown array variants — including their payload — into this constructor; the encoder re-emits the exact wire shape, so decoding/encoding round-trips.

Pairs naturally with [@@json.compact_variants] so the known cases are also bare strings.

type evt =
  | Login [@json.name "login"]
  | Click of int [@json.name "click"]
  | Unknown of Melange_json.unknown_variant_case [@json.catch_all]
[@@deriving json] [@@json.compact_variants]

The same syntax works for polymorphic variants:

type evt = [
  | `Login [@json.name "login"]
  | `Click of int [@json.name "click"]
  | `Unknown of Melange_json.unknown_variant_case [@json.catch_all]
] [@@deriving json] [@@json.compact_variants]
Wire shape mapping

payload distinguishes the wire shape so the value round-trips faithfully:

Wire JSONDecodedRe-encodes as
"future_tag"{ tag = "future_tag"; payload = None }"future_tag"
["future_tag"]{ tag = "future_tag"; payload = Some [] }["future_tag"]
["future_tag", 42]{ tag = "future_tag"; payload = Some [Int 42] }`["future_tag", 42]

[@@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.

JSON Schema ([@@deriving jsonschema])

The PPX can generate a JSON Schema from a type with [@@deriving jsonschema]. The schema matches the output produced by the json derivers.

open Ppx_deriving_jsonschema_runtime.Primitives.Melange_json

type t = {
  name: string;
  age: int;
} [@@deriving jsonschema]

let schema = Ppx_deriving_jsonschema_runtime.json_schema t_jsonschema

See JSONSCHEMA.md for the full documentation: setup, conversion rules, and all supported annotations.

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