package decoders-yojson

  1. Overview
  2. Docs

Description

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:

  1. Get your data from somewhere. Now you have a string.
  2. Parse the string as JSON (or YAML). Now you have a Yojson.Basic.json, or maybe an Ezjsonm.value, or perhaps a Ocaml.yaml.
  3. 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:

opam install decoders-ezjsonm      # For Ezjsonm
opam install decoders-ocyaml       # For Ocyaml
opam install decoders-yojson       # For Yojson
npm install --save-dev bs-decoders # For Bucklescript

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.json) 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 ints? 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

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 =
    field "name" string >>= fun name ->
    field "roles" (list role) >>= fun roles ->
    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.

Release

After updating CHANGES.md:

npm version <newversion>
dune-release tag
dune-release -p decoders,decoders-ezjsonm,decoders-yojson

Published: 28 Sep 2018

README

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.

An Ocaml program having a JSON (or YAML) data source usually goes something like this:

  1. Get your data from somewhere. Now you have a string.

  2. Parse the string as JSON (or YAML). Now you have a Yojson.Basic.json, or maybe an Ezjsonm.value, or perhaps a Ocaml.yaml.

  3. 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:

opam install decoders-ezjsonm      # For Ezjsonm
opam install decoders-ocyaml       # For Ocyaml
opam install decoders-yojson       # For Yojson
npm install --save-dev bs-decoders # For Bucklescript

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.json) 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 ints? 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

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 =
    field "name" string >>= fun name ->
    field "roles" (list role) >>= fun roles ->
    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.

Release

After updating CHANGES.md:

npm version <newversion>
dune-release tag
dune-release -p decoders,decoders-ezjsonm,decoders-yojson

Dependencies (4)

  1. yojson < "2.0.0"
  2. decoders < "0.3.0"
  3. jbuilder >= "1.0+beta9"
  4. ocaml

Dev Dependencies (1)

  1. ounit with-test

Used by (2)

  1. eris
  2. eris-lwt

Conflicts

None