1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
|
json-static
Introduction
============
json-static is a syntax extension of OCaml that can make the use of
JSON data easier. From a special type declaration, the camlp4 preprocessor
generates the code that converts between a JSON "abstract syntax tree"
and specialized OCaml data structures such as objects, polymorphic variants,
lists, arrays, tuples, etc.
It will at the same time check that the structure of the JSON document
is correct and produce OCaml data which is statically typed.
For example, the following declaration defines the type of a point object:
type json point = < x: float; y: float >
This automatically makes two functions available, with the following signature:
val json_of_point : point -> Json_type.t
val point_of_json : Json_type.t -> point
Json_type.t is the type of parsed JSON data, which is provided by
the json-wheel library.
Function json_of_point would convert an OCaml object of type point into
a JSON object. point_of_json works the other way around, and fails
by raising the Json_type.Json_error exception
if the input JSON data doesn't have the right format.
Installation
============
Installation:
make
make install
Uninstallation:
make uninstall
Usage
=====
Basically, you must preprocess your OCaml file(s) with
camlp4o pa_json_static.cmo. Once installed using the standard
procedure (ocamlfind), you can compile a file using these commands:
# compile
ocamlfind ocamlopt -c yourfile.ml -syntax camlp4o -package json-static
# link
ocamlfind ocamlopt -o yourprog yourfile.cmx -linkpkg -package json-wheel
Build tools like OCamlMakefile take care of this nicely.
Syntax
======
You must write a special type declaration that describes the expected
format of the JSON data. There is a predefined mapping from OCaml types
to JSON:
OCaml type JSON type Properties of JSON data
---------- --------- -----------------------
string String
float Number not an int
int Number an int
number* Number a float or an int
bool Boolean
list Array homogenous
array Array homogenous
tuple Array fixed length
(string * 'a) assoc** Object an object read as an associative list
(string, 'a) Hashtbl.t Object
object or record Object additional methods/fields are ignored
option any null means None
polymorphic variants String or Array a String for constructors without
an argument,
or an Array of length 2
where the first element is a
String that represents the
constructor
and the second element is the
argument.
classic variants String or Array a String for contructors without
an argument,
or an Array where the first element
is the String that represents the
constructor and the rest are the
arguments. Unlike polymorphic
variants, there may be several
arguments (just don't use parentheses
around them in the type definition).
X.t*** defined by X.of_json and X.to_json
---
*: the number type is an alias for float, but accepts JSON ints and converts
them to OCaml floats.
**: the assoc type is an alias for list, but converts from a JSON object.
***: X can be any simple module name, but module fields t, of_json and to_json
are mandatory.
A type definition is done like regular type definitions, but the keyword
"json" is placed just after "type", as in:
type json t = int * float
^^^^
The type cannot be polymorphic, i.e. it doesn't support type parameters.
A small set of basic types are supported (see table above). Other type
names can be used only if they are part of the same definition.
This works:
type json a = b
and b = int list
But the following doesn't work:
type json b = int list
type json a = b (* b is unknown to the preprocessor *)
In addition to the basic syntax for type declarations, a few extensions
have been added:
1) Object labels or variant names can be followed by an arbitrary string.
When it is the case, this defines the string to be found in the JSON
data.
2) Object methods can be preceded by a questionmark. If it is the case,
JSON objects that do not have this field could still be read.
Their value is set to null instead of being undefined and causing an error.
3) A default value can be specified a after a method definition.
The syntax is a "=" followed by an expression. The expression should
be a constant. The default expression is used whenever
the JSON null value is encountered, even if the type converter
knows how to deal with null.
4) Records and variant types must be named, as always in OCaml. Reusing
an existing definition is possible by using the "predefined" keyword
as in
type point = predefined { x : float; y : float }
or
type t = predefined A | B of bool * t
Predefined read-only records or variant types (private)
cannot be used as of version 0.9.3 of this program.
A common use of optional methods without a default argument is when omitting
an object field is allowed and considered equivalent to a null value.
Optional methods of a type that doesn't accept the null value don't make
much sense but are not rejected. The following is most likely an error:
type json point = < ?x : int >
(if field "x" is not found in the JSON object, then the error would be
that null is not a valid int, instead of saying that field "x" is missing)
It should be either
type json point = < ?x : int option >
or
type json point = < ?x : int = 0 >
Example 1
=========
The following definition is correct:
type json point = < x: number; y: number >
and coords = point array
It can load successfully the following JSON data:
[ { "x": 1, "y": 0.5 },
{ "x": 0, "y": 0.3333333 } ]
Full example:
(* File example1.ml *)
type json point = < x: number; y: number >
and coords = point array
let json_string = "
[ { \"x\": 1, \"y\": 0.5 },
{ \"x\": 0, \"y\": 0.3333333 } ]
"
let json_tree = Json_io.json_of_string json_string
let my_coords = coords_of_json json_tree
let _ = Array.iter (fun p -> Printf.printf "(%g, %g)\n" p#x p#y) my_coords
(* EOF *)
Save the example as "example1.ml", compile it and run it:
$ ocamlfind ocamlopt -o example1 -linkpkg -package json-static -syntax camlp4o example1.ml
$ ./example1
(1, 0.5)
(0, 0.333333)
Example 2
=========
This example shows you the representation that we chose for sum types in JSON:
(* File example2.ml *)
type json colors = [ `Black
| `White
| `Rgb of (float * float * float)
| `Any "*" ] list
let my_colors = [ `Black; `White; `Any;
`Rgb (1., 0., 0.);
`Rgb (0., 1., 0.);
`Rgb (0., 0., 1.) ]
let _ = print_endline (Json_io.string_of_json (json_of_colors my_colors))
(* EOF *)
$ ocamlfind ocamlopt -o example2 -linkpkg -package json-static -syntax camlp4o example2.ml
$ ./example2
[
"Black",
"White",
"*",
[ "Rgb",
[ 1.0,
0.0,
0.0 ]
],
[ "Rgb",
[ 0.0,
1.0,
0.0 ]
],
[ "Rgb",
[ 0.0,
0.0,
1.0 ]
]
]
Note how we specified that `Any translates into "*" rather than "Any".
The same technique is available to rename object methods, and it is crucial
when some existing JSON format uses method names that are not valid
OCaml identifiers.
|