Yaml encoder/decoder for OCaml jsont codecs
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** YAML codec using Jsont type descriptions.
7
8 This module provides YAML streaming encode/decode that interprets {!Jsont.t}
9 type descriptions, allowing the same codec definitions to work for both JSON
10 and YAML.
11
12 {b Example:}
13 {[
14 (* Define a codec once using Jsont *)
15 module Config = struct
16 type t = { name : string; port : int }
17
18 let make name port = { name; port }
19
20 let jsont =
21 Jsont.Object.map ~kind:"Config" make
22 |> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name)
23 |> Jsont.Object.mem "port" Jsont.int ~enc:(fun c -> c.port)
24 |> Jsont.Object.finish
25 end
26
27 (* Use the same codec for both JSON and YAML *)
28 let from_json = Jsont_bytesrw.decode_string Config.jsont json_str
29 let from_yaml = Yamlt.decode_string Config.jsont yaml_str
30 ]}
31
32 See notes about {{!yaml_mapping}YAML to JSON mapping},
33 {{!yaml_scalars}YAML scalar resolution}, and
34 {{!null_handling}null value handling}. *)
35
36open Bytesrw
37
38(** {1:decode Decode} *)
39
40val decode :
41 ?layout:bool ->
42 ?locs:bool ->
43 ?file:Jsont.Textloc.fpath ->
44 ?max_depth:int ->
45 ?max_nodes:int ->
46 'a Jsont.t ->
47 Bytes.Reader.t ->
48 ('a, string) result
49(** [decode t r] decodes a value from YAML reader [r] according to type [t].
50 - If [layout] is [true], style information is preserved in {!Jsont.Meta.t}
51 values (for potential round-tripping). Defaults to [false].
52 - If [locs] is [true], source locations are preserved in {!Jsont.Meta.t}
53 values and error messages are precisely located. Defaults to [false].
54 - [file] is the file path for error messages. Defaults to
55 {!Jsont.Textloc.file_none}.
56 - [max_depth] limits nesting depth to prevent stack overflow (billion laughs
57 protection). Defaults to [100].
58 - [max_nodes] limits total decoded nodes (billion laughs protection).
59 Defaults to [10_000_000].
60
61 The YAML input must contain exactly one document. Multi-document streams are
62 not supported; use {!decode_all} for those. *)
63
64val decode' :
65 ?layout:bool ->
66 ?locs:bool ->
67 ?file:Jsont.Textloc.fpath ->
68 ?max_depth:int ->
69 ?max_nodes:int ->
70 'a Jsont.t ->
71 Bytes.Reader.t ->
72 ('a, Jsont.Error.t) result
73(** [decode'] is like {!val-decode} but preserves the error structure. *)
74
75val decode_all :
76 ?layout:bool ->
77 ?locs:bool ->
78 ?file:Jsont.Textloc.fpath ->
79 ?max_depth:int ->
80 ?max_nodes:int ->
81 'a Jsont.t ->
82 Bytes.Reader.t ->
83 ('a, string) result Seq.t
84(** [decode_all t r] decodes all documents from a multi-document YAML stream.
85 Returns a sequence where each element is a result of decoding one document.
86 Parameters are as in {!val-decode}. Use this for YAML streams containing
87 multiple documents separated by [---]. *)
88
89val decode_all' :
90 ?layout:bool ->
91 ?locs:bool ->
92 ?file:Jsont.Textloc.fpath ->
93 ?max_depth:int ->
94 ?max_nodes:int ->
95 'a Jsont.t ->
96 Bytes.Reader.t ->
97 ('a, Jsont.Error.t) result Seq.t
98(** [decode_all'] is like {!val-decode_all} but preserves the error structure. *)
99
100(** {1:encode Encode} *)
101
102(** YAML output format. *)
103type yaml_format =
104 | Block (** Block style (indented) - default. Clean, readable YAML. *)
105 | Flow (** Flow style (JSON-like). Compact, single-line collections. *)
106 | Layout (** Preserve layout from {!Jsont.Meta.t} when available. *)
107
108val encode :
109 ?buf:Stdlib.Bytes.t ->
110 ?format:yaml_format ->
111 ?indent:int ->
112 ?explicit_doc:bool ->
113 ?scalar_style:Yamlrw.Scalar_style.t ->
114 'a Jsont.t ->
115 'a ->
116 eod:bool ->
117 Bytes.Writer.t ->
118 (unit, string) result
119(** [encode t v w] encodes value [v] according to type [t] to YAML on [w].
120 - If [buf] is specified, it is used as a buffer for output slices. Defaults
121 to a buffer of length {!Bytesrw.Bytes.Writer.slice_length}[ w].
122 - [format] controls the output style. Defaults to {!Block}.
123 - [indent] is the indentation width in spaces. Defaults to [2].
124 - [explicit_doc] if [true], emits explicit document markers ([---] and
125 [...]). Defaults to [false].
126 - [scalar_style] is the preferred style for string scalars. Defaults to
127 [`Any] (auto-detect based on content).
128 - [eod] indicates whether {!Bytesrw.Bytes.Slice.eod} should be written on
129 [w] after encoding. *)
130
131val encode' :
132 ?buf:Stdlib.Bytes.t ->
133 ?format:yaml_format ->
134 ?indent:int ->
135 ?explicit_doc:bool ->
136 ?scalar_style:Yamlrw.Scalar_style.t ->
137 'a Jsont.t ->
138 'a ->
139 eod:bool ->
140 Bytes.Writer.t ->
141 (unit, Jsont.Error.t) result
142(** [encode'] is like {!val-encode} but preserves the error structure. *)
143
144(** {1:recode Recode}
145
146 The defaults in these functions are those of {!val-decode} and
147 {!val-encode}, except if [layout] is [true], [format] defaults to {!Layout}
148 and vice-versa. *)
149
150val recode :
151 ?layout:bool ->
152 ?locs:bool ->
153 ?file:Jsont.Textloc.fpath ->
154 ?max_depth:int ->
155 ?max_nodes:int ->
156 ?buf:Stdlib.Bytes.t ->
157 ?format:yaml_format ->
158 ?indent:int ->
159 ?explicit_doc:bool ->
160 ?scalar_style:Yamlrw.Scalar_style.t ->
161 'a Jsont.t ->
162 Bytes.Reader.t ->
163 Bytes.Writer.t ->
164 eod:bool ->
165 (unit, string) result
166(** [recode t r w] is {!val-decode} followed by {!val-encode}. *)
167
168(** {1:yaml_mapping YAML to JSON Mapping}
169
170 YAML is a superset of JSON. This module maps YAML structures to the JSON
171 data model that {!Jsont.t} describes:
172
173 - YAML scalars map to JSON null, boolean, number, or string depending on
174 content and the expected type
175 - YAML sequences map to JSON arrays
176 - YAML mappings map to JSON objects (keys must be strings)
177 - YAML aliases are resolved during decoding
178 - YAML tags are used to guide type resolution when present
179
180 {b Limitations:}
181 - Only string keys are supported in mappings (JSON object compatibility)
182 - Anchors and aliases are resolved; the alias structure is not preserved
183 - Multi-document streams require {!decode_all} *)
184
185(** {1:yaml_scalars YAML Scalar Resolution}
186
187 YAML scalars are resolved to JSON types as follows:
188
189 {b Null:} [null], [Null], [NULL], [~], or empty string
190
191 {b Boolean:} [true], [True], [TRUE], [false], [False], [FALSE], [yes],
192 [Yes], [YES], [no], [No], [NO], [on], [On], [ON], [off], [Off], [OFF]
193
194 {b Number:} Decimal integers, floats, hex ([0x...]), octal ([0o...]),
195 infinity ([.inf], [-.inf]), NaN ([.nan])
196
197 {b String:} Anything else, or explicitly quoted scalars
198
199 When decoding against a specific {!Jsont.t} type, the expected type takes
200 precedence over automatic resolution. For example, decoding ["yes"] against
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*)