package ppx_deriving_decoders
Install
Dune Dependency
Authors
Maintainers
Sources
md5=9e806bddb897df2b8afedb51fd27a747
sha512=e72a298330f1bac8405c55db54e93c04085c1adaeb28fa416ebe1e86019733fc9cffef793a660e2a08a27b4230b1d66173af7d800b198622f11e543f3387d20c
Description
Using mattjbray/ocaml-decoders, use a ppx to automatically generate instances of a decoder for a particular type using PPX.
README
ppx_deriving_decoders: Automatically write mattjbray/ocaml-decoders
There are currently two major flavors of handling encoding and decoding data in OCaml.
You can use something like ppx_deriving_yojson to automatically generate encoders/decoders for your OCaml types, which works great! However, it gives some tough errors and there is limited customization of the decoders.
You can use the a library like mattjbray/ocaml-decoders to hand-write your encoders/decoders, which offers great errors and quite expansive customization! However, writing out encoders/decoders for all of your types is a lot of work.
What if there was a way to get the best of both worlds?
This library helps streamline the process of using mattjbray/ocaml-decoders by writing your encoders/decoders for you! Now, when you don't care about the implementation details of serializing/deserializing to e.g. JSON, you can just use the ppx to write the functions for you. But if you do care, you can generate a starting implementation and adjust it according to your preferences.
There are two primary ways in which this library can be of use. (More details of both follows.)
"I want to write a (e.g. JSON) decoder for a particular type but don't care about the details" --> You can then use this library via
[@@deriving decoders]
applied to your types."I want to write a (e.g. JSON) decoder for a particular type, but I care a lot about how it works and just want a good starting place" --> You can use this library via
[@@deriving_inline decoders]
applied to your types to generate the implementation in place.
Getting Started
opam install ppx_deriving_decoders
The implementation is agnostic to the underlying decoders back-end. The only requirement is the presence of a module with the signature Decoders.Decode.S
as specified in mattjbray/ocaml-decoders, which is aliased to module D
(for decoders, for encoders you need the corresponding implementation aliased to E
).
E.g., if you wanted to decode using yojson
, you could use
opam install decoders-yojson
Just generate the decoder for me
Suppose we have the following file:
(* In file foo.ml *)
type bar = Int of int | String of string
To generate a decoder for bar
, first add the preprocessing directive to the appropriate dune file:
(preprocess (pps ppx_deriving_decoders))
Then just add an implementer of Decoders.Decode.S
to the file, aliased to D
, and add the deriving extension:
(* In file foo.ml *)
module D = Decoders_yojson.Safe.Decode
type bar = Int of int | String of string [@@deriving decoders]
After doing this, you will have available in this module a value bar_decoder
of type bar D.decoder
. Then you'll be able to use this decoder freely, e.g.:
let () = assert (
match D.decode_string bar_decoder {|{"Int": 10}|} with
| Ok b -> b = Int 10
| Error _ -> false
)
Only get the decoder started for me
Suppose we have the same file again:
(* In file foo.ml *)
type bar = Int of int | String of string
To generate a decoder for bar
, we again first add the preprocessing directive to the appropriate dune file:
(preprocess (pps ppx_deriving_decoders))
We change the file to be
(* In file foo.ml *)
module D = Decoders_yojson.Safe.Decode
type bar = Int of int | String of string [@@deriving_inline decoders]
[@@@deriving.end]
Then, after running dune build --auto-promote
, our file will become (after applying ocamlformat
):
(* In file foo.ml *)
module D = Decoders_yojson.Safe.Decode
type bar = Int of int | String of string [@@deriving_inline decoders]
let _ = fun (_ : bar) -> ()
let bar_decoder =
let open D in
single_field (function
| "Int" -> D.int >|= fun arg -> Int arg
| "String" -> D.string >|= fun arg -> String arg
| any -> D.fail @@ Printf.sprintf "Unrecognized field: %s" any)
let _ = bar_decoder
[@@@deriving.end]
You can now freely remove the deriving attributes, and edit the decoder as you see fit!
More complicated example
The following file:
(* In file foo.ml *)
module D = Decoders_yojson.Safe.Decode
type expr = Num of int | BinOp of op * expr * expr
and op = Add | Sub | Mul | Div [@@deriving_inline decoders]
[@@@deriving.end]
after invoking dune build --auto-promote
(plus ocamlformat
) will yield:
(* In file foo.ml *)
type expr = Num of int | BinOp of op * expr * expr
and op = Add | Sub | Mul | Div [@@deriving_inline decoders]
let _ = fun (_ : expr) -> ()
let _ = fun (_ : op) -> ()
[@@@ocaml.warning "-27"]
let expr_decoder op_decoder =
D.fix (fun expr_decoder_aux ->
let open D in
single_field (function
| "Num" -> D.int >|= fun arg -> Num arg
| "BinOp" ->
let open D in
let ( >>=:: ) fst rest = uncons rest fst in
op_decoder >>=:: fun arg0 ->
expr_decoder_aux >>=:: fun arg1 ->
expr_decoder_aux >>=:: fun arg2 ->
succeed (BinOp (arg0, arg1, arg2))
| any -> D.fail @@ Printf.sprintf "Unrecognized field: %s" any))
let _ = expr_decoder
let op_decoder op_decoder =
let open D in
single_field (function
| "Add" -> succeed Add
| "Sub" -> succeed Sub
| "Mul" -> succeed Mul
| "Div" -> succeed Div
| any -> D.fail @@ Printf.sprintf "Unrecognized field: %s" any)
let _ = op_decoder
let op_decoder = D.fix op_decoder
let _ = op_decoder
let expr_decoder = expr_decoder op_decoder
let _ = expr_decoder
[@@@ocaml.warning "+27"]
[@@@deriving.end]
Notice that the mutual recursion is handled for you!
Type vars
The ppx
can also handle types with type variables:
type 'a wrapper = { wrapped : 'a } [@@deriving_inline decoders]
[@@@deriving.end]
becomes (additionally with ocamlformat
):
type 'a record_wrapper = { wrapped : 'a } [@@deriving_inline decoders]
let _ = fun (_ : 'a record_wrapper) -> ()
let record_wrapper_decoder a_decoder =
let open D in
let open D.Infix in
let* wrapped = field "wrapped" a_decoder in
succeed { wrapped }
let _ = record_wrapper_decoder
[@@@deriving.end]
Notice that the decoder for the type variable becomes a parameter of the generated decoder!
Encoders
All of the above information also applies to generating encoders. Using the above type as an example:
type 'a wrapper = { wrapped : 'a } [@@deriving_inline decoders]
[@@@deriving.end]
becomes (additionally with ocamlformat
):
type 'a wrapper = { wrapped : 'a } [@@deriving_inline encoders]
let _ = fun (_ : 'a record_wrapper) -> ()
let wrapper_encoder a_encoder { wrapped } =
E.obj [ ("wrapped", a_encoder wrapped) ]
let _ = record_wrapper_encoder
[@@@deriving.end]
Of course, you can generate both by using [@@deriving_inline decoders, encoders]
or [@@deriving decoders, encoders]
. The corresponding pair will be inverses of one another provided that all prior referenced decoder/encoder pairs are inverses!
Example Workflow
Suppose you wanted to start gathering trading data from Tiingo. So you navigate over to the End-of-Day Rest API Endpoint. You're going to need to decode this JSON. First what you're going to do is match your type exactly to the expected shape:
module EndOfDay = struct
type t = {
date : string;
close : float;
high : float;
low : float;
open : float;
volume : int;
adjClose : int;
adjHigh : float;
adjLow : float;
adjOpen : float;
adjVolume : int;
divCash : float;
splitFactor : float;
}
end
However, open
is a reserved keyword in OCaml, and the idiomatic solution is to append an underscore. Now you can apply your decoder:
module EndOfDay = struct
type t = {
date : string;
close : float;
high : float;
low : float;
open_ : float;
volume : int;
adjClose : int;
adjHigh : float;
adjLow : float;
adjOpen : float;
adjVolume : int;
divCash : float;
splitFactor : float;
}
[@@deriving decoders]
end
But of course, this is going to generate a decoder which expects a field called "open_"
rather than the intended "open"
! So, you customize your decoder by generating it inline:
module EndOfDay = struct
type t = {
date : string;
close : float;
high : float;
low : float;
open_ : float;
volume : int;
adjClose : int;
adjHigh : float;
adjLow : float;
adjOpen : float;
adjVolume : int;
divCash : float;
splitFactor : float;
}
[@@deriving_inline decoders]
[@@@deriving.end]
end
You apply dune build --auto-promote
(followed by ocamlformat
) and get:
module EndOfDay = struct
type t = {
date : string;
close : float;
high : float;
low : float;
open_ : float;
volume : int;
adjClose : int;
adjHigh : float;
adjLow : float;
adjOpen : float;
adjVolume : int;
divCash : float;
splitFactor : float;
}
[@@deriving_inline decoders]
let _ = fun (_ : t) -> ()
let t_decoder =
let open D in
let open D.Infix in
let* date = field "date" D.string in
let* close = field "close" D.float in
let* high = field "high" D.float in
let* low = field "low" D.float in
let* open_ = field "open_" D.float in
let* volume = field "volume" D.int in
let* adjClose = field "adjClose" D.int in
let* adjHigh = field "adjHigh" D.float in
let* adjLow = field "adjLow" D.float in
let* adjOpen = field "adjOpen" D.float in
let* adjVolume = field "adjVolume" D.int in
let* divCash = field "divCash" D.float in
let* splitFactor = field "splitFactor" D.float in
succeed
{
date;
close;
high;
low;
open_;
volume;
adjClose;
adjHigh;
adjLow;
adjOpen;
adjVolume;
divCash;
splitFactor;
}
let _ = t_decoder
[@@@deriving.end]
end
And now, fixing it is as easy as adjusting the argument to field
above for the value open_
!
module EndOfDay = struct
type t = {
date : string;
close : float;
high : float;
low : float;
open_ : float;
volume : int;
adjClose : int;
adjHigh : float;
adjLow : float;
adjOpen : float;
adjVolume : int;
divCash : float;
splitFactor : float;
}
let t_decoder =
let open D in
let open D.Infix in
let* date = field "date" D.string in
let* close = field "close" D.float in
let* high = field "high" D.float in
let* low = field "low" D.float in
let* open_ = field "open" D.float in
let* volume = field "volume" D.int in
let* adjClose = field "adjClose" D.int in
let* adjHigh = field "adjHigh" D.float in
let* adjLow = field "adjLow" D.float in
let* adjOpen = field "adjOpen" D.float in
let* adjVolume = field "adjVolume" D.int in
let* divCash = field "divCash" D.float in
let* splitFactor = field "splitFactor" D.float in
succeed
{
date;
close;
high;
low;
open_;
volume;
adjClose;
adjHigh;
adjLow;
adjOpen;
adjVolume;
divCash;
splitFactor;
}
end
And now you see, generating the appropriate decoder took no more than 5 seconds once ppx_deriving_decoders
is installed!
Limitations
Some of the decoders can be quite complicated relative to what you would write by hand
There are a lot of rough edges in places like:
Error reporting
Correctly handling
loc
In an ideal world, it would be nice to generate the corresponding decoders/encoders within their own submodule. It remains to be seen how this can be done.
Future Work
[ ] Simplify generated decoders
[ ] Generate decoders from a module
[ ] How to handle types produced from functors inline
Contributing
Contributions are always welcome. Please create an issue as appropriate, and open a PR into the main
branch and I'll have a look :)
Dependencies (5)
-
containers
>= "2.8"
-
decoders
>= "0.5.0"
-
ppxlib
>= "0.20.0"
-
dune
>= "3.11"
-
ocaml
>= "4.08.0"
Dev Dependencies (3)
-
odoc
with-doc
-
ppx_inline_test
with-test
-
decoders-yojson
with-test
Used by
None
Conflicts
None