Skip to main content

jsont

JSON type-safe encoding and decoding using the OCaml jsont library. Use when Claude needs to: define typed JSON codecs for OCaml record types, parse JSON strings to OCaml values, or serialize OCaml values to JSON, or work with nested JSON structures

Stars
31
Source
avsm/ocaml-claude-marketplace
Updated
2026-05-24
Slug
avsm--ocaml-claude-marketplace--jsont
View on GitHubRaw SKILL.md

// install — copy + paste into any project

mkdir -p .claude/skills && curl -fsSL https://raw.githubusercontent.com/avsm/ocaml-claude-marketplace/HEAD/plugins/ocaml-dev/skills/jsont/SKILL.md -o .claude/skills/jsont.md

Drops the SKILL.md into .claude/skills/jsont.md. Works with Claude Code, Cursor, and any agent that loads SKILL.md files from .claude/skills/.

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

  1. Always use ~kind: Provide descriptive kind names for better error messages
  2. Use skip_unknown for external APIs: Be tolerant of extra fields from third-party services
  3. Prefer opt_mem with defaults: Handle missing fields gracefully with Option.value ~default:
  4. Compose small codecs: Build complex structures from simple, reusable codecs
  5. Define helper functions: Create decode/encode helpers at module level for cleaner usage