Yaml encoder/decoder for OCaml jsont codecs

be more permissive about nulls mapping to collections

+207 -2
+24
lib/yamlt.ml
··· 201 201 else 202 202 (* Strings accept quoted scalars or non-null plain scalars *) 203 203 map.dec meta value 204 + | Array map -> 205 + (* Treat null as an empty array for convenience *) 206 + if is_null_scalar value then 207 + let end_meta = meta_of_span d ev.Event.span in 208 + map.dec_finish end_meta 0 (map.dec_empty ()) 209 + else 210 + err_type_mismatch d ev.span t ~fnd:"scalar" 211 + | Object map -> 212 + (* Treat null as an empty object for convenience *) 213 + if is_null_scalar value then 214 + (* Build a dict with all default values from absent members *) 215 + let add_default _ (Mem_dec mem_map) dict = 216 + match mem_map.dec_absent with 217 + | Some v -> Dict.add mem_map.id v dict 218 + | None -> 219 + (* Required field without default - error *) 220 + let exp = String_map.singleton mem_map.name (Mem_dec mem_map) in 221 + missing_mems_error meta map ~exp ~fnd:[] 222 + in 223 + let dict = String_map.fold add_default map.mem_decs Dict.empty in 224 + let dict = Dict.add object_meta_arg meta dict in 225 + apply_dict map.dec dict 226 + else 227 + err_type_mismatch d ev.span t ~fnd:"scalar" 204 228 | Map m -> 205 229 (* Handle Map combinators (e.g., from Jsont.option) *) 206 230 m.dec (decode_scalar_as d ev value style m.dom)
+52 -2
lib/yamlt.mli
··· 29 29 let from_yaml = Yamlt.decode_string Config.jsont yaml_str 30 30 ]} 31 31 32 - See notes about {{!yaml_mapping}YAML to JSON mapping} and 33 - {{!yaml_scalars}YAML scalar resolution}. *) 32 + See notes about {{!yaml_mapping}YAML to JSON mapping}, 33 + {{!yaml_scalars}YAML scalar resolution}, and 34 + {{!null_handling}null value handling}. *) 34 35 35 36 open Bytesrw 36 37 ··· 198 199 When decoding against a specific {!Jsont.t} type, the expected type takes 199 200 precedence over automatic resolution. For example, decoding ["yes"] against 200 201 {!Jsont.string} yields the string ["yes"], not [true]. *) 202 + 203 + (** {1:null_handling Null Value Handling} 204 + 205 + YAML null values are handled according to the expected type to provide 206 + friendly defaults while maintaining type safety: 207 + 208 + {b Collections (Arrays and Objects):} 209 + 210 + Null values decode as empty collections when the codec expects a collection 211 + type. This provides convenient defaults for optional collection fields in 212 + YAML: 213 + {[ 214 + # YAML with null collection fields 215 + config: 216 + items: null # Decodes as [] 217 + settings: ~ # Decodes as {} 218 + tags: # Missing value = null, decodes as [] 219 + ]} 220 + 221 + For arrays, null decodes to an empty array. For objects, null decodes to an 222 + object with all fields set to their [dec_absent] defaults. If any required 223 + field lacks a default, decoding fails with a missing member error. 224 + 225 + This behavior makes yamlt more forgiving for schemas with many optional 226 + collection fields, where writing [field:] (which parses as null) is natural 227 + and semantically equivalent to [field: []]. 228 + 229 + {b Numbers:} 230 + 231 + Null values decode to [Float.nan] when the codec expects a number. 232 + 233 + {b Primitive Types (Int, Bool, String):} 234 + 235 + Null values {e fail} when decoding into primitive scalar types ([int], 236 + [bool], [string]). Null typically indicates genuinely missing or incorrect 237 + data for these types, and silent conversion could clash with a manual 238 + setting of the default value (e.g. 0 and [null] for an integer would be 239 + indistinguishable). 240 + 241 + To accept null for primitive fields, explicitly use {!Jsont.option}: 242 + {[ 243 + (* Accepts null, decodes as None *) 244 + Jsont.Object.mem "count" (Jsont.option Jsont.int) ~dec_absent:None 245 + 246 + (* Rejects null, requires a number *) 247 + Jsont.Object.mem "count" Jsont.int ~dec_absent:0 248 + ]} 249 + 250 + *)
+5
tests/bin/dune
··· 42 42 (libraries yamlt jsont jsont.bytesrw bytesrw)) 43 43 44 44 (executable 45 + (name test_null_collections) 46 + (public_name test_null_collections) 47 + (libraries yamlt jsont jsont.bytesrw bytesrw)) 48 + 49 + (executable 45 50 (name test_opt_array) 46 51 (libraries yamlt jsont jsont.bytesrw bytesrw)) 47 52
+89
tests/bin/test_null_collections.ml
··· 1 + open Bytesrw 2 + 3 + let () = 4 + Printf.printf "=== Test 1: Explicit null as empty array ===\n"; 5 + let yaml1 = "values: null" in 6 + let codec1 = 7 + let open Jsont in 8 + Object.map ~kind:"Test" (fun v -> v) 9 + |> Object.mem "values" (list int) ~dec_absent:[] ~enc:(fun v -> v) 10 + |> Object.finish 11 + in 12 + (match Yamlt.decode codec1 (Bytes.Reader.of_string yaml1) with 13 + | Ok v -> 14 + Printf.printf "Result: [%s]\n" 15 + (String.concat "; " (List.map string_of_int v)) 16 + | Error e -> Printf.printf "Error: %s\n" e); 17 + 18 + Printf.printf "\n=== Test 2: Tilde as empty array ===\n"; 19 + let yaml2 = "values: ~" in 20 + (match Yamlt.decode codec1 (Bytes.Reader.of_string yaml2) with 21 + | Ok v -> 22 + Printf.printf "Result: [%s]\n" 23 + (String.concat "; " (List.map string_of_int v)) 24 + | Error e -> Printf.printf "Error: %s\n" e); 25 + 26 + Printf.printf "\n=== Test 3: Empty array syntax ===\n"; 27 + let yaml3 = "values: []" in 28 + (match Yamlt.decode codec1 (Bytes.Reader.of_string yaml3) with 29 + | Ok v -> 30 + Printf.printf "Result: [%s]\n" 31 + (String.concat "; " (List.map string_of_int v)) 32 + | Error e -> Printf.printf "Error: %s\n" e); 33 + 34 + Printf.printf "\n=== Test 4: Array with values ===\n"; 35 + let yaml4 = "values: [1, 2, 3]" in 36 + (match Yamlt.decode codec1 (Bytes.Reader.of_string yaml4) with 37 + | Ok v -> 38 + Printf.printf "Result: [%s]\n" 39 + (String.concat "; " (List.map string_of_int v)) 40 + | Error e -> Printf.printf "Error: %s\n" e); 41 + 42 + Printf.printf "\n=== Test 5: Explicit null as empty object ===\n"; 43 + let yaml5 = "config: null" in 44 + let codec2 = 45 + let open Jsont in 46 + let config_codec = 47 + Object.map ~kind:"Config" (fun timeout retries -> (timeout, retries)) 48 + |> Object.mem "timeout" int ~dec_absent:30 ~enc:fst 49 + |> Object.mem "retries" int ~dec_absent:3 ~enc:snd 50 + |> Object.finish 51 + in 52 + Object.map ~kind:"Test" (fun c -> c) 53 + |> Object.mem "config" config_codec ~dec_absent:(30, 3) ~enc:(fun c -> c) 54 + |> Object.finish 55 + in 56 + (match Yamlt.decode codec2 (Bytes.Reader.of_string yaml5) with 57 + | Ok (timeout, retries) -> 58 + Printf.printf "Result: {timeout=%d; retries=%d}\n" timeout retries 59 + | Error e -> Printf.printf "Error: %s\n" e); 60 + 61 + Printf.printf "\n=== Test 6: Empty object syntax ===\n"; 62 + let yaml6 = "config: {}" in 63 + (match Yamlt.decode codec2 (Bytes.Reader.of_string yaml6) with 64 + | Ok (timeout, retries) -> 65 + Printf.printf "Result: {timeout=%d; retries=%d}\n" timeout retries 66 + | Error e -> Printf.printf "Error: %s\n" e); 67 + 68 + Printf.printf "\n=== Test 7: Object with values ===\n"; 69 + let yaml7 = "config:\n timeout: 60\n retries: 5" in 70 + (match Yamlt.decode codec2 (Bytes.Reader.of_string yaml7) with 71 + | Ok (timeout, retries) -> 72 + Printf.printf "Result: {timeout=%d; retries=%d}\n" timeout retries 73 + | Error e -> Printf.printf "Error: %s\n" e); 74 + 75 + Printf.printf "\n=== Test 8: Nested null arrays ===\n"; 76 + let yaml8 = "name: test\nitems: null\ntags: ~" in 77 + let codec3 = 78 + let open Jsont in 79 + Object.map ~kind:"Nested" (fun name items tags -> (name, items, tags)) 80 + |> Object.mem "name" string ~enc:(fun (n, _, _) -> n) 81 + |> Object.mem "items" (list int) ~dec_absent:[] ~enc:(fun (_, i, _) -> i) 82 + |> Object.mem "tags" (list string) ~dec_absent:[] ~enc:(fun (_, _, t) -> t) 83 + |> Object.finish 84 + in 85 + match Yamlt.decode codec3 (Bytes.Reader.of_string yaml8) with 86 + | Ok (name, items, tags) -> 87 + Printf.printf "Result: {name=%s; items_count=%d; tags_count=%d}\n" 88 + name (List.length items) (List.length tags) 89 + | Error e -> Printf.printf "Error: %s\n" e
+37
tests/cram/null_collections.t
··· 1 + Null to Empty Collection Tests 2 + ================================ 3 + 4 + This test suite validates that yamlt treats null values as empty collections 5 + when decoding into Array or Object types, providing a more user-friendly 6 + YAML experience. 7 + 8 + ================================================================================ 9 + NULL AS EMPTY COLLECTION 10 + ================================================================================ 11 + 12 + Test various forms of null decoding as empty arrays and objects 13 + 14 + $ test_null_collections 15 + === Test 1: Explicit null as empty array === 16 + Result: [] 17 + 18 + === Test 2: Tilde as empty array === 19 + Result: [] 20 + 21 + === Test 3: Empty array syntax === 22 + Result: [] 23 + 24 + === Test 4: Array with values === 25 + Result: [1; 2; 3] 26 + 27 + === Test 5: Explicit null as empty object === 28 + Result: {timeout=30; retries=3} 29 + 30 + === Test 6: Empty object syntax === 31 + Result: {timeout=30; retries=3} 32 + 33 + === Test 7: Object with values === 34 + Result: {timeout=60; retries=5} 35 + 36 + === Test 8: Nested null arrays === 37 + Result: {name=test; items_count=0; tags_count=0}