A batteries included HTTP/1.1 client in OCaml

Add typed JSON codec support with Body.jsonv and Response.jsonv

- Body.jsonv: encode typed values to JSON using Jsont.t codecs
- Response.jsonv: decode JSON responses to typed values using Jsont.t codecs

These functions provide type-safe JSON serialization/deserialization,
complementing the existing untyped Jsont.json-based functions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+73
+8
lib/body.ml
··· 53 53 in 54 54 String { content; mime = Mime.json } 55 55 56 + (* Typed JSON encoding using a Jsont.t codec *) 57 + let jsonv (type a) (codec : a Jsont.t) (value : a) = 58 + let content = match Jsont_bytesrw.encode_string' ~format:Jsont.Minify codec value with 59 + | Ok s -> s 60 + | Error e -> json_encoding_error e 61 + in 62 + String { content; mime = Mime.json } 63 + 56 64 (* JSON streaming using jsont - we encode the value to string and stream it *) 57 65 module Json_stream_source = struct 58 66 type t = {
+25
lib/body.mli
··· 79 79 ]} 80 80 *) 81 81 82 + val jsonv : 'a Jsont.t -> 'a -> t 83 + (** [jsonv codec value] creates a JSON body by encoding [value] using the 84 + typed [codec]. The value is encoded to a minified JSON string with 85 + Content-Type: application/json. 86 + 87 + This is the preferred way to create JSON bodies from typed OCaml values, 88 + as it provides type safety and works with custom record types. 89 + 90 + Example: 91 + {[ 92 + (* Define a codec for your type *) 93 + type user = { name : string; age : int } 94 + 95 + let user_codec = 96 + Jsont.Obj.map ~kind:"user" (fun name age -> { name; age }) 97 + |> Jsont.Obj.mem "name" Jsont.string ~enc:(fun u -> u.name) 98 + |> Jsont.Obj.mem "age" Jsont.int ~enc:(fun u -> u.age) 99 + |> Jsont.Obj.finish 100 + 101 + (* Create a JSON body from a typed value *) 102 + let body = Body.jsonv user_codec { name = "Alice"; age = 30 } 103 + ]} 104 + 105 + @raise Eio.Io with {!Error.Json_encode_error} if encoding fails. *) 106 + 82 107 val json_stream : Jsont.json -> t 83 108 (** [json_stream json_value] creates a streaming JSON body from a Jsont.json value. 84 109 The JSON value will be encoded to a minified JSON string and streamed.
+13
lib/response.ml
··· 181 181 reason = Jsont.Error.to_string e 182 182 })) 183 183 184 + let jsonv (type a) (codec : a Jsont.t) t = 185 + let body_str = text t in 186 + match Jsont_bytesrw.decode_string' codec body_str with 187 + | Ok value -> value 188 + | Error e -> 189 + let preview = if String.length body_str > 200 190 + then String.sub body_str 0 200 191 + else body_str in 192 + raise (Error.err (Error.Json_parse_error { 193 + body_preview = preview; 194 + reason = Jsont.Error.to_string e 195 + })) 196 + 184 197 let raise_for_status t = 185 198 if t.status >= 400 then 186 199 raise (Error.err (Error.Http_error {
+27
lib/response.mli
··· 224 224 @raise Eio.Io with {!Error.Json_parse_error} if JSON parsing fails. 225 225 @raise Failure if the response has already been closed. *) 226 226 227 + val jsonv : 'a Jsont.t -> t -> 'a 228 + (** [jsonv codec response] parses the response body as JSON and decodes it 229 + to a typed value using the provided [codec]. 230 + The response body is fully consumed by this operation. 231 + 232 + This is the preferred way to decode JSON responses into typed OCaml values, 233 + as it provides type safety and works with custom record types. 234 + 235 + Example: 236 + {[ 237 + (* Define a codec for your type *) 238 + type user = { name : string; age : int } 239 + 240 + let user_codec = 241 + Jsont.Obj.map ~kind:"user" (fun name age -> { name; age }) 242 + |> Jsont.Obj.mem "name" Jsont.string ~enc:(fun u -> u.name) 243 + |> Jsont.Obj.mem "age" Jsont.int ~enc:(fun u -> u.age) 244 + |> Jsont.Obj.finish 245 + 246 + (* Decode the response to a typed value *) 247 + let user = Response.jsonv user_codec response in 248 + Printf.printf "User: %s, age %d\n" user.name user.age 249 + ]} 250 + 251 + @raise Eio.Io with {!Error.Json_parse_error} if JSON parsing fails. 252 + @raise Failure if the response has already been closed. *) 253 + 227 254 val raise_for_status : t -> t 228 255 (** [raise_for_status response] raises [Eio.Io] with [Error.Http_error] if the 229 256 response status code indicates an error (>= 400). Returns the response