Yaml encoder/decoder for OCaml jsont codecs

be more permissive about nulls mapping to collections

Changed files
+207 -2
lib
tests
+24
lib/yamlt.ml
···
else
(* Strings accept quoted scalars or non-null plain scalars *)
map.dec meta value
| Map m ->
(* Handle Map combinators (e.g., from Jsont.option) *)
m.dec (decode_scalar_as d ev value style m.dom)
···
else
(* Strings accept quoted scalars or non-null plain scalars *)
map.dec meta value
+
| Array map ->
+
(* Treat null as an empty array for convenience *)
+
if is_null_scalar value then
+
let end_meta = meta_of_span d ev.Event.span in
+
map.dec_finish end_meta 0 (map.dec_empty ())
+
else
+
err_type_mismatch d ev.span t ~fnd:"scalar"
+
| Object map ->
+
(* Treat null as an empty object for convenience *)
+
if is_null_scalar value then
+
(* Build a dict with all default values from absent members *)
+
let add_default _ (Mem_dec mem_map) dict =
+
match mem_map.dec_absent with
+
| Some v -> Dict.add mem_map.id v dict
+
| None ->
+
(* Required field without default - error *)
+
let exp = String_map.singleton mem_map.name (Mem_dec mem_map) in
+
missing_mems_error meta map ~exp ~fnd:[]
+
in
+
let dict = String_map.fold add_default map.mem_decs Dict.empty in
+
let dict = Dict.add object_meta_arg meta dict in
+
apply_dict map.dec dict
+
else
+
err_type_mismatch d ev.span t ~fnd:"scalar"
| Map m ->
(* Handle Map combinators (e.g., from Jsont.option) *)
m.dec (decode_scalar_as d ev value style m.dom)
+52 -2
lib/yamlt.mli
···
let from_yaml = Yamlt.decode_string Config.jsont yaml_str
]}
-
See notes about {{!yaml_mapping}YAML to JSON mapping} and
-
{{!yaml_scalars}YAML scalar resolution}. *)
open Bytesrw
···
When decoding against a specific {!Jsont.t} type, the expected type takes
precedence over automatic resolution. For example, decoding ["yes"] against
{!Jsont.string} yields the string ["yes"], not [true]. *)
···
let from_yaml = Yamlt.decode_string Config.jsont yaml_str
]}
+
See notes about {{!yaml_mapping}YAML to JSON mapping},
+
{{!yaml_scalars}YAML scalar resolution}, and
+
{{!null_handling}null value handling}. *)
open Bytesrw
···
When decoding against a specific {!Jsont.t} type, the expected type takes
precedence over automatic resolution. For example, decoding ["yes"] against
{!Jsont.string} yields the string ["yes"], not [true]. *)
+
+
(** {1:null_handling Null Value Handling}
+
+
YAML null values are handled according to the expected type to provide
+
friendly defaults while maintaining type safety:
+
+
{b Collections (Arrays and Objects):}
+
+
Null values decode as empty collections when the codec expects a collection
+
type. This provides convenient defaults for optional collection fields in
+
YAML:
+
{[
+
# YAML with null collection fields
+
config:
+
items: null # Decodes as []
+
settings: ~ # Decodes as {}
+
tags: # Missing value = null, decodes as []
+
]}
+
+
For arrays, null decodes to an empty array. For objects, null decodes to an
+
object with all fields set to their [dec_absent] defaults. If any required
+
field lacks a default, decoding fails with a missing member error.
+
+
This behavior makes yamlt more forgiving for schemas with many optional
+
collection fields, where writing [field:] (which parses as null) is natural
+
and semantically equivalent to [field: []].
+
+
{b Numbers:}
+
+
Null values decode to [Float.nan] when the codec expects a number.
+
+
{b Primitive Types (Int, Bool, String):}
+
+
Null values {e fail} when decoding into primitive scalar types ([int],
+
[bool], [string]). Null typically indicates genuinely missing or incorrect
+
data for these types, and silent conversion could clash with a manual
+
setting of the default value (e.g. 0 and [null] for an integer would be
+
indistinguishable).
+
+
To accept null for primitive fields, explicitly use {!Jsont.option}:
+
{[
+
(* Accepts null, decodes as None *)
+
Jsont.Object.mem "count" (Jsont.option Jsont.int) ~dec_absent:None
+
+
(* Rejects null, requires a number *)
+
Jsont.Object.mem "count" Jsont.int ~dec_absent:0
+
]}
+
+
*)
+5
tests/bin/dune
···
(libraries yamlt jsont jsont.bytesrw bytesrw))
(executable
(name test_opt_array)
(libraries yamlt jsont jsont.bytesrw bytesrw))
···
(libraries yamlt jsont jsont.bytesrw bytesrw))
(executable
+
(name test_null_collections)
+
(public_name test_null_collections)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
(name test_opt_array)
(libraries yamlt jsont jsont.bytesrw bytesrw))
+89
tests/bin/test_null_collections.ml
···
···
+
open Bytesrw
+
+
let () =
+
Printf.printf "=== Test 1: Explicit null as empty array ===\n";
+
let yaml1 = "values: null" in
+
let codec1 =
+
let open Jsont in
+
Object.map ~kind:"Test" (fun v -> v)
+
|> Object.mem "values" (list int) ~dec_absent:[] ~enc:(fun v -> v)
+
|> Object.finish
+
in
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml1) with
+
| Ok v ->
+
Printf.printf "Result: [%s]\n"
+
(String.concat "; " (List.map string_of_int v))
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 2: Tilde as empty array ===\n";
+
let yaml2 = "values: ~" in
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml2) with
+
| Ok v ->
+
Printf.printf "Result: [%s]\n"
+
(String.concat "; " (List.map string_of_int v))
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 3: Empty array syntax ===\n";
+
let yaml3 = "values: []" in
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml3) with
+
| Ok v ->
+
Printf.printf "Result: [%s]\n"
+
(String.concat "; " (List.map string_of_int v))
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 4: Array with values ===\n";
+
let yaml4 = "values: [1, 2, 3]" in
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml4) with
+
| Ok v ->
+
Printf.printf "Result: [%s]\n"
+
(String.concat "; " (List.map string_of_int v))
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 5: Explicit null as empty object ===\n";
+
let yaml5 = "config: null" in
+
let codec2 =
+
let open Jsont in
+
let config_codec =
+
Object.map ~kind:"Config" (fun timeout retries -> (timeout, retries))
+
|> Object.mem "timeout" int ~dec_absent:30 ~enc:fst
+
|> Object.mem "retries" int ~dec_absent:3 ~enc:snd
+
|> Object.finish
+
in
+
Object.map ~kind:"Test" (fun c -> c)
+
|> Object.mem "config" config_codec ~dec_absent:(30, 3) ~enc:(fun c -> c)
+
|> Object.finish
+
in
+
(match Yamlt.decode codec2 (Bytes.Reader.of_string yaml5) with
+
| Ok (timeout, retries) ->
+
Printf.printf "Result: {timeout=%d; retries=%d}\n" timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 6: Empty object syntax ===\n";
+
let yaml6 = "config: {}" in
+
(match Yamlt.decode codec2 (Bytes.Reader.of_string yaml6) with
+
| Ok (timeout, retries) ->
+
Printf.printf "Result: {timeout=%d; retries=%d}\n" timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 7: Object with values ===\n";
+
let yaml7 = "config:\n timeout: 60\n retries: 5" in
+
(match Yamlt.decode codec2 (Bytes.Reader.of_string yaml7) with
+
| Ok (timeout, retries) ->
+
Printf.printf "Result: {timeout=%d; retries=%d}\n" timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 8: Nested null arrays ===\n";
+
let yaml8 = "name: test\nitems: null\ntags: ~" in
+
let codec3 =
+
let open Jsont in
+
Object.map ~kind:"Nested" (fun name items tags -> (name, items, tags))
+
|> Object.mem "name" string ~enc:(fun (n, _, _) -> n)
+
|> Object.mem "items" (list int) ~dec_absent:[] ~enc:(fun (_, i, _) -> i)
+
|> Object.mem "tags" (list string) ~dec_absent:[] ~enc:(fun (_, _, t) -> t)
+
|> Object.finish
+
in
+
match Yamlt.decode codec3 (Bytes.Reader.of_string yaml8) with
+
| Ok (name, items, tags) ->
+
Printf.printf "Result: {name=%s; items_count=%d; tags_count=%d}\n"
+
name (List.length items) (List.length tags)
+
| Error e -> Printf.printf "Error: %s\n" e
+37
tests/cram/null_collections.t
···
···
+
Null to Empty Collection Tests
+
================================
+
+
This test suite validates that yamlt treats null values as empty collections
+
when decoding into Array or Object types, providing a more user-friendly
+
YAML experience.
+
+
================================================================================
+
NULL AS EMPTY COLLECTION
+
================================================================================
+
+
Test various forms of null decoding as empty arrays and objects
+
+
$ test_null_collections
+
=== Test 1: Explicit null as empty array ===
+
Result: []
+
+
=== Test 2: Tilde as empty array ===
+
Result: []
+
+
=== Test 3: Empty array syntax ===
+
Result: []
+
+
=== Test 4: Array with values ===
+
Result: [1; 2; 3]
+
+
=== Test 5: Explicit null as empty object ===
+
Result: {timeout=30; retries=3}
+
+
=== Test 6: Empty object syntax ===
+
Result: {timeout=30; retries=3}
+
+
=== Test 7: Object with values ===
+
Result: {timeout=60; retries=5}
+
+
=== Test 8: Nested null arrays ===
+
Result: {name=test; items_count=0; tags_count=0}