Install and the two variants
Install.
$ opam install yojson
dune, for a library that reads and writes JSON.
(library(name myapp)(libraries yojson))
Yojson ships two JSON ASTs. Basic.t is strict JSON; Safe.t adds variants for values JSON has no native representation for - large ints, tuples, variants - which round-trip through a convention rather than the spec.
type Basic.t =[ `Null| `Bool of bool| `Int of int| `Float of float| `String of string| `Assoc of (string * Basic.t) list| `List of Basic.t list]type Safe.t =[ Basic.t| `Intlit of string| `Tuple of Safe.t list| `Variant of string * Safe.t option]
Safe.t is the default nearly everyone reaches for - it is a strict superset, and ppx_deriving_yojson (covered below) targets it.
open Yojson.Safe
Building a value by hand
Every constructor written out, for a small object.
let config : Yojson.Safe.t =`Assoc[ ("host", `String "localhost"); ("port", `Int 8080); ("debug", `Bool false); ("tags", `List [ `String "web"; `String "prod" ]); ("timeout", `Null)]
Serializing to a string, or pretty-printed.
# Yojson.Safe.to_string config;;- : string ="{\"host\":\"localhost\",\"port\":8080,\"debug\":false,\"tags\":[\"web\",\"prod\"],\"timeout\":null}"# Yojson.Safe.pretty_to_string config;;- : string ="{\n \"host\": \"localhost\",\n \"port\": 8080,\n ...\n}"
Writing directly to a file or channel, without building an intermediate string.
let () = Yojson.Safe.to_file "config.json" configlet () = Yojson.Safe.to_channel stdout configlet () = Yojson.Safe.pretty_to_channel stdout config
Parsing
from_string raises Yojson.Json_error on malformed input rather than returning an option - wrap it if the input is untrusted.
let v = Yojson.Safe.from_string{|{"host": "localhost", "port": 8080}|}
A malformed document.
# Yojson.Safe.from_string "{not json";;Exception: Yojson.Json_error "Line 1, bytes 1-4:\nInvalid token 'not'"
from_file and from_channel, for reading directly off disk or a socket.
let v = Yojson.Safe.from_file "config.json"let v = Yojson.Safe.from_channel stdin
A safe wrapper returning a result instead of raising, the shape most application code actually wants at a boundary.
let parse_safely (s : string) : (Yojson.Safe.t, string) result =try Ok (Yojson.Safe.from_string s)with Yojson.Json_error msg -> Error msg
Pattern matching directly on the AST
Yojson.Safe.t is a plain polymorphic variant, so ordinary match handles any shape with no library call at all - this is the lowest-level way to pull a value out.
let get_port (v : Yojson.Safe.t) : int option =match v with| `Assoc fields ->(match List.assoc_opt "port" fields with| Some (`Int n) -> Some n| _ -> None)| _ -> None
A recursive walk - counting every string value anywhere in a document, regardless of nesting depth.
let rec count_strings (v : Yojson.Safe.t) : int =match v with| `String _ -> 1| `List xs -> List.fold_left (fun acc x -> acc + count_strings x) 0 xs| `Assoc fields ->List.fold_left (fun acc (_, v) -> acc + count_strings v) 0 fields| `Tuple xs -> List.fold_left (fun acc x -> acc + count_strings x) 0 xs| _ -> 0
Rewriting a document in place - a pure recursive transform that doubles every int it finds, leaving everything else untouched.
let rec double_ints (v : Yojson.Safe.t) : Yojson.Safe.t =match v with| `Int n -> `Int (n * 2)| `List xs -> `List (List.map double_ints xs)| `Assoc fields -> `Assoc (List.map (fun (k, v) -> (k, double_ints v)) fields)| other -> other
Yojson.Safe.Util
member looks up a key on an Assoc, raising Type_error if the value is not an object at all - the common navigation function, and the one everything below chains off.
open Yojson.Safe.Utillet v = Yojson.Safe.from_string{|{"user": {"name": "ada", "age": 30}}|}# v |> member "user" |> member "name";;- : Yojson.Safe.t = `String "ada"
The to_* extractors pull a typed OCaml value out of a Yojson.Safe.t, raising Type_error if the shape does not match.
let name = v |> member "user" |> member "name" |> to_stringlet age = v |> member "user" |> member "age" |> to_int(* name : string = "ada" *)(* age : int = 30 *)
The full extractor set worth knowing.
to_string to_int to_float to_boolto_list to_assoc to_number (int or float)to_option f applies f unless the value is `Null, then Noneto_string_option, to_int_option, ... same idea, one per type
A missing key raises Type_error too - member on a key that is not present returns `Null, and to_string on `Null then raises.
# v |> member "missing" |> to_string;;Exception: Yojson.Safe.Util.Type_error ("Expected string, got null", `Null)
Handling optional fields with to_option, or with the dedicated *_option extractors - the idiomatic way to read a field that may legitimately be absent.
let timeout = v |> member "timeout" |> to_int_option(* timeout : int option = None, no exception *)
Navigating arrays: index for a specific position, to_list plus List.map for every element.
let v = Yojson.Safe.from_string {|{"tags": ["web", "prod", "api"]}|}let first_tag = v |> member "tags" |> index 0 |> to_stringlet all_tags = v |> member "tags" |> to_list |> List.map to_string
filter_* helpers narrow a list of Yojson.Safe.t to only the ones matching a shape - useful over a heterogeneous list where not every element is expected to be a string.
let mixed = [ `String "a"; `Int 1; `String "b"; `Null ]# filter_string mixed;;- : string list = ["a"; "b"]
ppx_deriving_yojson
Install, and wire the ppx in through dune.
$ opam install ppx_deriving_yojson
dune.
(executable(name main)(libraries yojson)(preprocess (pps ppx_deriving_yojson)))
One attribute generates both directions, for a plain record - no Util calls to write by hand.
type config = {host : string;port : int;debug : bool;tags : string list;} [@@deriving yojson](* generates: *)(* val config_to_yojson : config -> Yojson.Safe.t *)(* val config_of_yojson : Yojson.Safe.t -> (config, string) result *)
Round-tripping.
let c = { host = "localhost"; port = 8080; debug = false; tags = ["a"; "b"] }# config_to_yojson c |> Yojson.Safe.to_string;;- : string = "{\"host\":\"localhost\",\"port\":8080,\"debug\":false,\"tags\":[\"a\",\"b\"]}"# Yojson.Safe.from_string {|{"host":"x","port":1,"debug":true,"tags":[]}|}|> config_of_yojson;;- : (config, string) result = Ok {host = "x"; port = 1; debug = true; tags = []}
Field-level attributes: key renames the JSON field, default supplies a value so absence is not an error, and [@yojson.option] turns a field into an option that maps to/from a missing key rather than JSON null.
type request = {method_ : string [@key "method"];path : string;timeout : int [@default 30];body : string option [@yojson.option];} [@@deriving yojson]
Nested records derive independently and compose - a field whose type is itself @@deriving yojson just works, with no extra annotation at the use site.
type user = { name : string; age : int } [@@deriving yojson]type response = {status : int;data : user;} [@@deriving yojson]
Sum types derive too, tagged by constructor name under a `+kind` key by default.
type shape =| Circle of float| Rect of float * float[@@deriving yojson]# Circle 5.0 |> shape_to_yojson |> Yojson.Safe.to_string;;- : string = "[\"Circle\",5.0]"
Deriving on a variant used as a Map/Hashtbl key or elsewhere Ord/Eq/Show are also wanted - stack multiple derivers in one clause, exactly the same pattern the ppx post covers for any deriver.
type shape =| Circle of float| Rect of float * float[@@deriving yojson, eq, show]
Error handling at the boundary
config_of_yojson returns Error with a message rather than raising - chain it the same way any other result-returning parser is chained.
let load_config path : (config, string) result =match Yojson.Safe.from_file path with| exception Yojson.Json_error msg -> Error ("invalid json: " ^ msg)| exception Sys_error msg -> Error ("cannot read file: " ^ msg)| json -> config_of_yojson json
Using it.
# load_config "config.json";;- : (config, string) result = Ok {host = "localhost"; ...}# load_config "missing.json";;- : (config, string) result = Error "cannot read file: missing.json: No such file or directory"
Streaming large documents
Both Basic.t and Safe.t build the entire document in memory before returning. For a document too large to hold at once - a multi-gigabyte log export, a paginated dump - Yojson's stream module reads one top-level value at a time.
A file holding one JSON value per line, read lazily rather than parsed as one array.
let process_line (v : Yojson.Safe.t) =match v with| `Assoc fields ->(match List.assoc_opt "level" fields with| Some (`String "ERROR") -> print_endline "found an error line"| _ -> ())| _ -> ()let () =let ic = open_in "events.ndjson" in(trywhile true dolet line = input_line ic inprocess_line (Yojson.Safe.from_string line)donewith End_of_file -> ());close_in ic
For a genuine single large JSON array rather than newline-delimited objects, Yojson.Safe.stream_from_channel yields each top-level value from a sequence of concatenated JSON values without holding the whole input in memory - correct only when the source really is several JSON texts back to back, not one big array.
let () =let ic = open_in "concatenated.json" inYojson.Safe.stream_from_channel ic|> Stream.iter process_line;close_in ic
Basic vs Safe, and when it matters
A large integer literal - one that does not fit in a native OCaml int - is the case where the two variants actually diverge in practice.
# Yojson.Basic.from_string "99999999999999999999";;- : Yojson.Basic.t = `Int ... (* silently truncated / platform dependent *)# Yojson.Safe.from_string "99999999999999999999";;- : Yojson.Safe.t = `Intlit "99999999999999999999"
Converting between the two ASTs when a library only speaks Basic.t - to_basic drops anything Basic cannot represent, so it is lossy on Intlit/Tuple/Variant.
let safe_v : Yojson.Safe.t = `Intlit "99999999999999999999"# Yojson.Safe.to_basic safe_v;;- : Yojson.Basic.t = `String "99999999999999999999"
The reverse direction is always safe, since Basic.t is a strict subset - every constructor already exists in Safe.t.
let basic_v : Yojson.Basic.t = `Int 5let promoted : Yojson.Safe.t = (basic_v :> Yojson.Safe.t)
Manual traversal is fine for one-off scripts, but the moment a type has more than a couple of fields, generating the (de)serializer with ppx_deriving_yojson beats hand-writing it. For how that kind of deriver actually works under the hood, see ppx in Practice.