Jsont JSON Encoding/Decoding
Dependencies
(libraries jsont jsont.bytesrw)
Core Patterns
Simple Object Codec
Map a JSON object to an OCaml record using Jsont.Object.map with mem for required fields:
type header = {
message_id : string;
method_ : string;
timestamp : int;
}
let header_codec =
Jsont.Object.map ~kind:"header"
(fun message_id method_ timestamp -> { message_id; method_; timestamp })
|> Jsont.Object.mem "messageId" Jsont.string ~enc:(fun h -> h.message_id)
|> Jsont.Object.mem "method" Jsont.string ~enc:(fun h -> h.method_)
|> Jsont.Object.mem "timestamp" Jsont.int ~enc:(fun h -> h.timestamp)
|> Jsont.Object.finish
Optional Fields
Use opt_mem for optional JSON fields. The constructor receives 'a option:
type config = {
name : string;
timeout : int; (* default if missing *)
}
let config_codec =
Jsont.Object.map ~kind:"config"
(fun name timeout_opt ->
{ name; timeout = Option.value ~default:30 timeout_opt })
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name)
|> Jsont.Object.opt_mem "timeout" Jsont.int ~enc:(fun c -> Some c.timeout)
|> Jsont.Object.finish
Skip Unknown Fields
Use skip_unknown before finish to ignore extra JSON fields (tolerant parsing):
let tolerant_codec =
Jsont.Object.map ~kind:"data" (fun id -> { id })
|> Jsont.Object.mem "id" Jsont.string ~enc:(fun d -> d.id)
|> Jsont.Object.skip_unknown (* ignore extra fields *)
|> Jsont.Object.finish
Nested Objects
Compose codecs for nested structures:
type request = { header : header; payload : payload }
let request_codec payload_codec =
Jsont.Object.map ~kind:"request" (fun header payload -> { header; payload })
|> Jsont.Object.mem "header" header_codec ~enc:(fun r -> r.header)
|> Jsont.Object.mem "payload" payload_codec ~enc:(fun r -> r.payload)
|> Jsont.Object.finish
Lists
Use Jsont.list for JSON arrays:
type response = { items : item list }
let response_codec =
Jsont.Object.map ~kind:"response" (fun items -> { items })
|> Jsont.Object.mem "items" (Jsont.list item_codec) ~enc:(fun r -> r.items)
|> Jsont.Object.finish
String Maps
Use Jsont.Object.as_string_map for objects with dynamic keys:
module String_map = Map.Make(String)
(* JSON: {"key1": "value1", "key2": "value2"} *)
let string_map_codec = Jsont.Object.as_string_map Jsont.string
(* JSON: {"group1": [...], "group2": [...]} *)
let groups_codec = Jsont.Object.as_string_map (Jsont.list item_codec)
Empty Object
For payloads that don't carry data:
let empty_payload_codec : unit Jsont.t =
Jsont.Object.map ~kind:"empty" ()
|> Jsont.Object.skip_unknown
|> Jsont.Object.finish
Custom Value Mapping
Use Jsont.map to transform between types:
type device_type = Sonos | Meross | Other
let device_from_string =
Jsont.map ~kind:"device_type"
~dec:(function "sonos" -> Sonos | "meross" -> Meross | _ -> Other)
~enc:(function Sonos -> "sonos" | Meross -> "meross" | Other -> "other")
Jsont.string
Polymorphic Decoding with any
Handle multiple JSON shapes for backwards compatibility:
(* Device can be string (old format) or object (new format) *)
let device_compat_codec =
Jsont.any ~kind:"device"
~dec_string:device_from_string_codec (* handles "192.168.1.1" *)
~dec_object:device_object_codec (* handles {"ip": "...", "type": "..."} *)
~enc:(fun _ -> device_object_codec) (* always encode as object *)
()
Null Values
Use Jsont.null for endpoints returning null:
(* For DELETE endpoints that return null on success *)
match delete http ~sw token endpoint (Jsont.null ()) with
| Ok () -> ...
Generic JSON
Use Jsont.json to preserve arbitrary JSON:
type characteristic = {
iid : int;
value : Jsont.json option; (* preserve any JSON value *)
}
let char_codec =
Jsont.Object.map ~kind:"char" (fun iid value -> { iid; value })
|> Jsont.Object.mem "iid" Jsont.int ~enc:(fun c -> c.iid)
|> Jsont.Object.opt_mem "value" Jsont.json ~enc:(fun c -> c.value)
|> Jsont.Object.finish
Encoding and Decoding
Use Jsont_bytesrw for string-based encoding/decoding:
(* Decode JSON string to OCaml value *)
let decode codec s = Jsont_bytesrw.decode_string codec s
(* Returns: ('a, Jsont.Error.t) result *)
(* Encode OCaml value to JSON string *)
let encode codec v =
match Jsont_bytesrw.encode_string codec v with
| Ok s -> s
| Error _ -> "{}" (* fallback for encoding errors *)
(* Usage *)
match Jsont_bytesrw.decode_string config_codec json_string with
| Ok config -> (* use config *)
| Error e -> (* handle error *)
match Jsont_bytesrw.encode_string config_codec config with
| Ok json_str -> (* send json_str *)
| Error _ -> (* handle error *)
Common Helpers
Define module-level helpers for cleaner code:
let decode codec s = Jsont_bytesrw.decode_string codec s
let encode codec v =
match Jsont_bytesrw.encode_string codec v with
| Ok s -> s
| Error _ -> ""
Base Types Reference
| OCaml Type | Jsont Codec | JSON Type |
|---|---|---|
string |
Jsont.string |
string |
int |
Jsont.int |
number |
float |
Jsont.number |
number |
bool |
Jsont.bool |
boolean |
'a list |
Jsont.list codec |
array |
'a option |
Jsont.option codec |
value or null |
unit |
Jsont.null () |
null |
| generic | Jsont.json |
any JSON |
Good and Bad Examples
Missing ~kind
Bad: No kind makes error messages unhelpful
let config_codec =
Jsont.Object.map (fun name -> { name }) (* No ~kind *)
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name)
|> Jsont.Object.finish
(* Error: "expected string" - but where? *)
Good: Descriptive kind for clear errors
let config_codec =
Jsont.Object.map ~kind:"config" (fun name -> { name })
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name)
|> Jsont.Object.finish
(* Error: "config: expected string for member 'name'" *)
Strict vs Tolerant Parsing
Bad: Strict parsing breaks when API adds fields
(* API adds "created_at" field, your code breaks *)
let user_codec =
Jsont.Object.map ~kind:"user" (fun id name -> { id; name })
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun u -> u.id)
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun u -> u.name)
|> Jsont.Object.finish (* No skip_unknown! *)
Good: Tolerant parsing ignores unknown fields
let user_codec =
Jsont.Object.map ~kind:"user" (fun id name -> { id; name })
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun u -> u.id)
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun u -> u.name)
|> Jsont.Object.skip_unknown (* Ignore extra fields *)
|> Jsont.Object.finish
Handling Optional Fields
Bad: Nullable field without default causes runtime errors
(* Field missing → decode fails *)
let config_codec =
Jsont.Object.map ~kind:"config" (fun timeout -> { timeout })
|> Jsont.Object.mem "timeout" Jsont.int ~enc:(fun c -> c.timeout)
|> Jsont.Object.finish
Good: Use opt_mem with sensible default
let config_codec =
Jsont.Object.map ~kind:"config"
(fun timeout_opt -> { timeout = Option.value ~default:30 timeout_opt })
|> Jsont.Object.opt_mem "timeout" Jsont.int ~enc:(fun c -> Some c.timeout)
|> Jsont.Object.finish
Codec Composition
Bad: Monolithic codec with duplicated patterns
let request_codec =
Jsont.Object.map ~kind:"request"
(fun msg_id method_ ts payload_id payload_data ->
{ header = { message_id = msg_id; method_; timestamp = ts };
payload = { id = payload_id; data = payload_data } })
|> Jsont.Object.mem "messageId" Jsont.string ~enc:(fun r -> r.header.message_id)
|> Jsont.Object.mem "method" Jsont.string ~enc:(fun r -> r.header.method_)
(* ... lots more fields mixed together *)
Good: Compose small, reusable codecs
let header_codec =
Jsont.Object.map ~kind:"header"
(fun message_id method_ timestamp -> { message_id; method_; timestamp })
|> Jsont.Object.mem "messageId" Jsont.string ~enc:(fun h -> h.message_id)
|> Jsont.Object.mem "method" Jsont.string ~enc:(fun h -> h.method_)
|> Jsont.Object.mem "timestamp" Jsont.int ~enc:(fun h -> h.timestamp)
|> Jsont.Object.finish
let request_codec =
Jsont.Object.map ~kind:"request" (fun header payload -> { header; payload })
|> Jsont.Object.mem "header" header_codec ~enc:(fun r -> r.header)
|> Jsont.Object.mem "payload" payload_codec ~enc:(fun r -> r.payload)
|> Jsont.Object.finish
Encoding Error Handling
Bad: Silently returning empty object on error
let encode codec v =
match Jsont_bytesrw.encode_string codec v with
| Ok s -> s
| Error _ -> "{}" (* Hides the error! *)
Good: Propagate or log encoding errors
let encode codec v =
match Jsont_bytesrw.encode_string codec v with
| Ok s -> Ok s
| Error e ->
Log.err (fun m -> m "JSON encode failed: %a" Jsont.Error.pp e);
Error (`Encode_error e)
Best Practices
- Always use
~kind: Provide descriptive kind names for better error messages - Use
skip_unknownfor external APIs: Be tolerant of extra fields from third-party services - Prefer
opt_memwith defaults: Handle missing fields gracefully withOption.value ~default: - Compose small codecs: Build complex structures from simple, reusable codecs
- Define helper functions: Create
decode/encodehelpers at module level for cleaner usage