AoAH Day 7: Converting between JSON and Yaml with yamlt / Dec 2025
After the excitement of building an entire
Approach
I used all the previous tricks learnt so far, with all the dependent libraries available in the source tree for the agent to browse. The source code to jsont was absolutely essential, since the agent needed to learn how to traverse the jsont codecs to build a different (yaml) translator.
One thing that was essential was guidance on how to do certain conversions where the results are ambiguous. For example, null handling in Yaml is much more permissive than in JSON, so I opted for any dictionary or array types to resolve nulls in the Yaml to the empty dictionary. It would also be ok to just raise an error in that case, but it seems safer to try to recover from what a human might do.
Tests
As before, cram tests are a very effective way of exercising codepaths. I got the agent to generate a comprehensive suite of cram tests that do things like:
Encode arrays to JSON and YAML formats
$ test_arrays encode
JSON: {"numbers":[1,2,3,4,5],"strings":["hello","world"]}
YAML Block:
numbers:
- 1
- 2
- 3
- 4
- 5
strings:
- hello
- world
YAML Flow: {numbers: [1, 2, 3, 4, 5], strings: [hello, world]}
And also negative results:
Attempting to decode an object file with an array codec should fail
$ test_arrays int ../data/objects/simple.yml
JSON: int_array: ERROR: Missing member values in Numbers object
File "-", line 1, characters 0-28:
YAML: int_array: ERROR: Missing member values in Numbers object
File "-":
Which is a convenient way of checking that location tracking is working.
Results
This all worked fairly straightforwardly. The Yamlt interface even includes support for multidocument Yaml files:
val decode_all :
?layout:bool ->
?locs:bool ->
?file:Jsont.Textloc.fpath ->
?max_depth:int ->
?max_nodes:int ->
'a Jsont.t ->
Bytes.Reader.t ->
('a, string) result Seq.t
(** [decode_all t r] decodes all documents from a multi-document YAML stream.
Returns a sequence where each element is a result of decoding one document.
Parameters are as in {!val-decode}. Use this for YAML streams containing
multiple documents separated by [---]. *)
There's a lot going on this function, but it's easy to use. The optional parameters control details like Yaml layout and location tracking and defence against billion laughs attacks, but ultimately accept the jsont codec and a Yaml bytesrw source and give the result back as a lazy Seq.t.
Reflections
Using this library is very easy. The example code illustrates this:
module Config = struct
type t = { name : string; port : int }
let make name port = { name; port }
let jsont =
Jsont.Object.map ~kind:"Config" make
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name)
|> Jsont.Object.mem "port" Jsont.int ~enc:(fun c -> c.port)
|> Jsont.Object.finish
end
(* Use the same codec for both JSON and YAML *)
let from_json = Jsont_bytesrw.decode_string Config.jsont json_str
let from_yaml = Yamlt.decode_string Config.jsont yaml_str
I've started using yamlt in my website code without issue. It's very convenient being able to describe a format as a jsont codec (which take care of conversion to OCaml types and a concrete wire format), but then also exposing this as Yaml for easier human editing. The line number tracking means that it's much easier for me to trace back errors, thanks to jsont being so careful about this in its implementation and interface.
I also notice that
