Awesome
ocaml-decoders: Elm-inspired decoders for OCaml
A combinator library for "decoding" JSON-like values into your own OCaml types, inspired by Elm's Json.Decode
and Json.Encode
.
Eh?
An OCaml program having a JSON (or YAML) data source usually goes something like this:
- Get your data from somewhere. Now you have a
string
. - Parse the
string
as JSON (or YAML). Now you have aYojson.Basic.t
, or maybe anEzjsonm.value
. - Decode the JSON value to an OCaml type that's actually useful for your program's domain.
This library helps with step 3.
Getting started
Install one of the supported decoder backends:
For ocaml
opam install decoders-bencode # For bencode
opam install decoders-cbor # For CBOR
opam install decoders-ezjsonm # For ezjsonm
opam install decoders-jsonm # For jsonm
opam install decoders-msgpck # For msgpck
opam install decoders-sexplib # For sexplib
opam install decoders-yojson # For yojson
For bucklescript
npm install --save-dev bs-decoders
Decoding
Now we can start decoding stuff!
First, a module alias to save some keystrokes. In this guide, we'll parse JSON
using Yojson
's Basic
variant.
utop # module D = Decoders_yojson.Basic.Decode;;
module D = Decoders_yojson.Basic.Decode
Let's set our sights high and decode an integer.
utop # D.decode_value D.int (`Int 1);;
- : (int, error) result = Ok 1
Nice! We used decode_value
, which takes a decoder
and a value
(in this
case a Yojson.Basic.t
) and... decodes the value.
utop # D.decode_value;;
- : 'a decoder -> value -> ('a, error) result = <fun>
For convenience we also have decode_string
, which takes a string
and calls
Yojson
's parser under the hood.
utop # D.decode_string D.int "1";;
- : (int, error) result = Ok 1
What about a list
of int
s? Here's where the "combinator" part comes in.
utop # D.decode_string D.(list int) "[1,2,3]";;
- : (int list, error) result = Ok [1; 2; 3]
Success!
Ok, so what if we get some unexpected JSON?
utop # #install_printer D.pp_error;;
utop # D.decode_string D.(list int) "[1,2,true]";;
- : (int list, error) result =
Error while decoding a list: element 2: Expected an int, but got true
Complicated JSON structure
To decode a JSON object with many fields, we can use the let-binding operators
(let*
, etc.) from the Infix
module.
type my_user =
{ name : string
; age : int
}
let my_user_decoder : my_user decoder =
let open D in
let* name = field "name" string in
let* age = field "age" int in
succeed { name; age }
Note for Bucklescript users: let-binding operators are not currently available in Bucklescript, so if you need your decoders to be compatible with Bucklescript you can use the monadic bind operator (
>>=
):let my_user_decoder : my_user decoder = let open D in field "name" string >>= fun name -> field "age" int >>= fun age -> succeed { name; age }
We can also use these operators to decode objects with inconsistent structure. Say, for example, our JSON is a list of shapes. Squares have a side length, circles have a radius, and triangles have a base and a height.
[{ "shape": "square", "side": 11 },
{ "shape": "circle", "radius": 5 },
{ "shape": "triange", "base": 3, "height": 7 }]
We could represent these types in OCaml and decode them like this:
type square = { side : int }
type circle = { radius : int }
type triangle = { base : int; height : int }
type shape =
| Square of square
| Circle of circle
| Triangle of triangle
let square_decoder : square decoder =
D.(let+ s = field "side" int in { side = s })
let circle_decoder : circle decoder =
D.(let+ r = field "radius" int in { radius = r })
let triangle_decoder : triangle decoder =
D.(
let* b = field "base" int in
let+ h = field "height" int in
{ base = b; height = h })
let shape_decoder : shape decoder =
let open D in
let* shape = field "shape" string in
match shape with
| "square" -> let+ s = square_decoder in Square s
| "circle" -> let+ c = circle_decoder in Circle c
| "triangle" -> let+ t = triangle_decoder in Triangle t
| _ -> fail "Expected a shape"
let decode_list (json_string : string) : (shape list, _) result =
D.(decode_string (list shape_decoder) json_string)
Now, say that we didn't have the benefit of the "shape"
field describing the
type of the shape in our JSON list. We can still decode the shapes by trying
each decoder in turn using the one_of
combinator.
one_of
takes a list of string * 'a decoder
pairs and tries each decoder in
turn. The string
element of each pair is just used to name the decoder in
error messages.
let shape_decoder_2 : shape decoder =
D.(
one_of
[ ("a square", let+ s = square_decoder in Square s)
; ("a circle", let+ c = circle_decoder in Circle c)
; ("a triangle", let+ t = triangle_decoder in Triangle t)
]
)
Generic decoders
Suppose our program deals with users and roles. We want to decode our JSON input into these types.
type role = Admin | User
type user =
{ name : string
; roles : role list
}
Let's define our decoders. We'll write a module functor so we can re-use the same decoders across different JSON libraries, with YAML input, or with Bucklescript.
module My_decoders(D : Decoders.Decode.S) = struct
open D
let role : role decoder =
string >>= function
| "ADMIN" -> succeed Admin
| "USER" -> succeed User
| _ -> fail "Expected a role"
let user : user decoder =
let* name = field "name" string in
let* roles = field "roles" (list role) in
succeed { name; roles }
end
module My_yojson_decoders = My_decoders(Decoders_yojson.Basic.Decode)
Great! Let's try them out.
utop # open My_yojson_decoders;;
utop # D.decode_string role {| "USER" |};;
- : (role, error) result = Ok User
utop # D.decode_string D.(field "users" (list user))
{| {"users": [{"name": "Alice", "roles": ["ADMIN", "USER"]},
{"name": "Bob", "roles": ["USER"]}]}
|};;
- : (user list, error) result =
Ok [{name = "Alice"; roles = [Admin; User]}; {name = "Bob"; roles = [User]}]
Let's introduce an error in the JSON:
utop # D.decode_string D.(field "users" (list user))
{| {"users": [{"name": "Alice", "roles": ["ADMIN", "USER"]},
{"name": "Bob", "roles": ["SUPER_USER"]}]}
|};;
- : (user list, error) result =
Error
in field "users":
while decoding a list:
element 1:
in field "roles":
while decoding a list:
element 0: Expected a role, but got "SUPER_USER"
We get a nice pointer that we forgot to handle the SUPER_USER
role.
Encoding
ocaml-decoders
also has support for defining backend-agnostic encoders, for
turning your OCaml values into JSON values.
module My_encoders(E : Decoders.Encode.S) = struct
open E
let role : role encoder =
function
| Admin -> string "ADMIN"
| User -> string "USER"
let user : user encoder =
fun u ->
obj
[ ("name", string u.name)
; ("roles", list role u.roles)
]
end
module My_yojson_encoders = My_encoders(Decoders_yojson.Basic.Encode)
utop # module E = Decoders_yojson.Basic.Encode;;
utop # open My_yojson_encoders;;
utop # let users =
[ {name = "Alice"; roles = [Admin; User]}
; {name = "Bob"; roles = [User]}
];;
utop # E.encode_string E.obj [("users", E.list user users)];;
- : string =
"{\"users\":[{\"name\":\"Alice\",\"roles\":[\"ADMIN\",\"USER\"]},{\"name\":\"Bob\",\"roles\":[\"USER\"]}]}"
API Documentation
For more details, see the API documentation:
Decoders.Decode.S
interfaceDecoders.Encode.S
interface
Decoding XML
A similar decoders interface exists for decoding XML. See the interface file src/xml.ml
for documentation.
XML implementations
Platform | Package | Module | Example usage |
---|---|---|---|
opam | decoders-ezxmlm | Decoders_ezxmlm.Decode | src-ezxmlm/test/test_ezxmlm_decode.ml |
npm | bs-decoders | Decoders.Bs_xml.Decode | __tests__/decoders_bs_xml_test.ml |
Release
After updating CHANGES.md:
npm version <newversion> # e.g. npm version 0.7.0
git push --tags
dune-release
npm publish