this repo has no description
1opam-version: "2.0"
2synopsis: "Elm-inspired decoders for Ocaml"
3description: """
4A combinator library for "decoding" JSON-like values into your own Ocaml types, inspired by Elm's `Json.Decode` and `Json.Encode`.
5
6> Eh?
7
8An Ocaml program having a JSON (or YAML) data source usually goes something like this:
9
101. Get your data from somewhere. Now you have a `string`.
112. *Parse* the `string` as JSON (or YAML). Now you have a `Yojson.Basic.json`, or maybe an `Ezjsonm.value`, or perhaps a `Ocaml.yaml`.
123. *Decode* the JSON value to an Ocaml type that's actually useful for your program's domain.
13
14This library helps with step 3.
15
16## Getting started
17
18Install one of the supported decoder backends:
19
20### For ocaml
21
22```
23opam install decoders-ezjsonm # For Ezjsonm
24opam install decoders-ocyaml # For Ocyaml
25opam install decoders-yojson # For Yojson
26```
27
28### For bucklescript
29
30You'll need `cppo` available on your path for bs-decoders to build. If you've got an `opam` switch (for `merlin` etc) it's likely to already be installed, otherwise you can `opam install cppo`. Then just run `eval $(opam env)` before kicking off the bucklescript build.
31
32```
33npm install --save-dev bs-decoders
34```
35
36## Decoding
37
38Now we can start decoding stuff!
39
40First, a module alias to save some keystrokes. In this guide, we'll parse JSON
41using `Yojson`'s `Basic` variant.
42
43```ocaml
44utop # module D = Decoders_yojson.Basic.Decode;;
45module D = Decoders_yojson.Basic.Decode
46```
47
48Let's set our sights high and decode an integer.
49
50```ocaml
51utop # D.decode_value D.int (`Int 1);;
52- : (int, error) result = Ok 1
53```
54
55Nice! We used `decode_value`, which takes a `decoder` and a `value` (in this
56case a `Yojson.Basic.json`) and... decodes the value.
57
58```ocaml
59utop # D.decode_value;;
60- : 'a decoder -> value -> ('a, error) result = <fun>
61```
62
63For convenience we also have `decode_string`, which takes a `string` and calls
64`Yojson`'s parser under the hood.
65
66```ocaml
67utop # D.decode_string D.int "1";;
68- : (int, error) result = Ok 1
69```
70
71What about a `list` of `int`s? Here's where the "combinator" part comes in.
72
73```ocaml
74utop # D.decode_string D.(list int) "[1,2,3]";;
75- : (int list, error) result = Ok [1; 2; 3]
76```
77
78Success!
79
80Ok, so what if we get some unexpected JSON?
81
82```ocaml
83utop # #install_printer D.pp_error;;
84utop # D.decode_string D.(list int) "[1,2,true]";;
85- : (int list, error) result =
86Error while decoding a list: element 2: Expected an int, but got true
87```
88
89## Complicated JSON structure
90
91To decode a JSON object with many fields, we can use the bind operator (`>>=`) from the `Infix` module.
92
93```ocaml
94type my_user =
95 { name : string
96 ; age : int
97 }
98
99let my_user_decoder : my_user decoder =
100 let open D in
101 field "name" string >>= fun name ->
102 field "age" int >>= fun age ->
103 succeed { name; age }
104```
105
106We can also use bind to decode objects with inconsistent structure. Say, for
107example, our JSON is a list of shapes. Squares have a side length, circles have
108a radius, and triangles have a base and a height.
109
110```json
111[{ "shape": "square", "side": 11 },
112 { "shape": "circle", "radius": 5 },
113 { "shape": "triange", "base": 3, "height": 7 }]
114```
115
116We could represent these types in OCaml and decode them like this:
117
118```ocaml
119type shape =
120 | Square of int
121 | Circle of int
122 | Triangle of int * int
123
124let square_decoder : shape decoder =
125 D.(field "side" int >>= fun s -> succeed (Square side))
126
127let circle_decoder : shape decoder =
128 D.(field "radius" int >>= fun r -> succeed (Circle r))
129
130let triangle_decoder : shape decoder =
131 D.(
132 field "base" int >>= fun b ->
133 field "height" int >>= fun h ->
134 succeed (Triangle (b, h))
135 )
136
137let shape_decoder : shape decoder =
138 let open D in
139 field "shape" string >>= function
140 | "square" -> square_decoder
141 | "circle" -> circle_decoder
142 | "triangle" -> triangle_decoder
143 | _ -> fail "Expected a shape"
144
145
146let decode_list (json_string : string) : (shape list, _) result =
147 D.(decode_string (list shape_decoder) json_string)
148```
149
150Now, say that we didn't have the benefit of the `"shape"` field describing the
151type of the shape in our JSON list. We can still decode the shapes by trying
152each decoder in turn using the `one_of` combinator.
153
154`one_of` takes a list of `string * 'a decoder` pairs and tries each decoder in
155turn. The `string` element of each pair is just used to name the decoder in
156error messages.
157
158```ocaml
159let shape_decoder_2 : shape decoder =
160 D.(
161 one_of
162 [ ("a square", sqaure_decoder)
163 ; ("a circle", circle_decoder)
164 ; ("a triangle", triangle_decoder)
165 ]
166 )
167```
168
169## Generic decoders
170
171
172Suppose our program deals with users and roles. We want to decode our JSON input
173into these types.
174
175```ocaml
176type role = Admin | User
177
178type user =
179 { name : string
180 ; roles : role list
181 }
182```
183
184Let's define our decoders. We'll write a module functor so we can re-use the
185same decoders across different JSON libraries, with YAML input, or with
186Bucklescript.
187
188```ocaml
189module My_decoders(D : Decoders.Decode.S) = struct
190 open D
191
192 let role : role decoder =
193 string >>= function
194 | "ADMIN" -> succeed Admin
195 | "USER" -> succeed User
196 | _ -> fail "Expected a role"
197
198 let user : user decoder =
199 field "name" string >>= fun name ->
200 field "roles" (list role) >>= fun roles ->
201 succeed { name; roles }
202end
203
204module My_yojson_decoders = My_decoders(Decoders_yojson.Basic.Decode)
205```
206
207Great! Let's try them out.
208
209```ocaml
210utop # open My_yojson_decoders;;
211utop # D.decode_string role {| "USER" |};;
212- : (role, error) result = Ok User
213
214utop # D.decode_string D.(field "users" (list user))
215 {| {"users": [{"name": "Alice", "roles": ["ADMIN", "USER"]},
216 {"name": "Bob", "roles": ["USER"]}]}
217 |};;
218- : (user list, error) result =
219Ok [{name = "Alice"; roles = [Admin; User]}; {name = "Bob"; roles = [User]}]
220```
221
222Let's introduce an error in the JSON:
223
224```ocaml
225utop # D.decode_string D.(field "users" (list user))
226 {| {"users": [{"name": "Alice", "roles": ["ADMIN", "USER"]},
227 {"name": "Bob", "roles": ["SUPER_USER"]}]}
228 |};;
229- : (user list, error) result =
230Error
231 in field "users":
232 while decoding a list:
233 element 1:
234 in field "roles":
235 while decoding a list:
236 element 0: Expected a role, but got "SUPER_USER"
237```
238
239We get a nice pointer that we forgot to handle the `SUPER_USER` role.
240
241## Release
242
243After updating CHANGES.md:
244
245```
246npm version <newversion>
247dune-release tag
248dune-release -p decoders,decoders-ezjsonm,decoders-yojson
249```"""
250maintainer: "Matt Bray <matt@aestheticintegration.com>"
251authors: "Matt Bray <matt@aestheticintegration.com>"
252license: "ISC"
253homepage: "https://github.com/mattjbray/ocaml-decoders"
254doc: "https://mattjbray.github.io/ocaml-decoders/decoders-ezjsonm"
255bug-reports: "https://github.com/mattjbray/ocaml-decoders/issues"
256depends: [
257 "ocaml"
258 "jbuilder" {>= "1.0+beta7"}
259 "ounit" {with-test}
260 "decoders" {< "0.3.0"}
261 "ezjsonm" {>= "0.4.0"}
262]
263build: [
264 ["jbuilder" "build" "-p" name "-j" jobs]
265 ["jbuilder" "runtest" "-p" name "-j" jobs] {with-test}
266]
267dev-repo: "git+ssh://git@github.com/mattjbray/ocaml-decoders.git"
268url {
269 src:
270 "https://github.com/mattjbray/ocaml-decoders/releases/download/0.1.1/decoders-0.1.1.tbz"
271 checksum: [
272 "sha256=9c9e0a579d96eeeb245cc833c4d534fe3382b68d5ad46db81c04cd1f6cf893f7"
273 "md5=8b17539d73a1a6867868d2024a0d11a1"
274 ]
275}