From ocaml-dev
Defines type-safe JSON codecs for OCaml records using jsont library. Parses JSON to values, serializes OCaml to JSON, handles nested structures, lists, optional fields, and unknown skips.
npx claudepluginhub avsm/ocaml-claude-marketplace --plugin ocaml-devThis skill uses the workspace's default tool permissions.
```dune
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
(libraries jsont jsont.bytesrw)
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
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
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
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
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
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)
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
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
anyHandle 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 *)
()
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 () -> ...
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
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 *)
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 _ -> ""
| 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 |
~kindBad: 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'" *)
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
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
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
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)
~kind: Provide descriptive kind names for better error messagesskip_unknown for external APIs: Be tolerant of extra fields from third-party servicesopt_mem with defaults: Handle missing fields gracefully with Option.value ~default:decode/encode helpers at module level for cleaner usage