blob: fc10246bdb2353ebe0aa98ea6b4cc00421071c61 [file] [log] [blame]
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +05301/*
2Copyright 2021 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
Zack Williamse940c7a2019-08-21 14:25:39 -070017package yaml
18
19import (
20 "bytes"
21 "encoding/json"
22 "fmt"
David Bainbridge86971522019-09-26 22:09:39 +000023 "io"
Zack Williamse940c7a2019-08-21 14:25:39 -070024 "reflect"
25 "strconv"
26
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053027 "sigs.k8s.io/yaml/goyaml.v2"
Zack Williamse940c7a2019-08-21 14:25:39 -070028)
29
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053030// Marshal marshals obj into JSON using stdlib json.Marshal, and then converts JSON to YAML using JSONToYAML (see that method for more reference)
31func Marshal(obj interface{}) ([]byte, error) {
32 jsonBytes, err := json.Marshal(obj)
Zack Williamse940c7a2019-08-21 14:25:39 -070033 if err != nil {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053034 return nil, fmt.Errorf("error marshaling into JSON: %w", err)
Zack Williamse940c7a2019-08-21 14:25:39 -070035 }
36
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053037 return JSONToYAML(jsonBytes)
Zack Williamse940c7a2019-08-21 14:25:39 -070038}
39
David Bainbridge86971522019-09-26 22:09:39 +000040// JSONOpt is a decoding option for decoding from JSON format.
41type JSONOpt func(*json.Decoder) *json.Decoder
42
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053043// Unmarshal first converts the given YAML to JSON, and then unmarshals the JSON into obj. Options for the
44// standard library json.Decoder can be optionally specified, e.g. to decode untyped numbers into json.Number instead of float64, or to disallow unknown fields (but for that purpose, see also UnmarshalStrict). obj must be a non-nil pointer.
45//
46// Important notes about the Unmarshal logic:
47//
48// - Decoding is case-insensitive, unlike the rest of Kubernetes API machinery, as this is using the stdlib json library. This might be confusing to users.
49// - This decodes any number (although it is an integer) into a float64 if the type of obj is unknown, e.g. *map[string]interface{}, *interface{}, or *[]interface{}. This means integers above +/- 2^53 will lose precision when round-tripping. Make a JSONOpt that calls d.UseNumber() to avoid this.
50// - Duplicate fields, including in-case-sensitive matches, are ignored in an undefined order. Note that the YAML specification forbids duplicate fields, so this logic is more permissive than it needs to. See UnmarshalStrict for an alternative.
51// - Unknown fields, i.e. serialized data that do not map to a field in obj, are ignored. Use d.DisallowUnknownFields() or UnmarshalStrict to override.
52// - As per the YAML 1.1 specification, which yaml.v2 used underneath implements, literal 'yes' and 'no' strings without quotation marks will be converted to true/false implicitly.
53// - YAML non-string keys, e.g. ints, bools and floats, are converted to strings implicitly during the YAML to JSON conversion process.
54// - There are no compatibility guarantees for returned error values.
55func Unmarshal(yamlBytes []byte, obj interface{}, opts ...JSONOpt) error {
56 return unmarshal(yamlBytes, obj, yaml.Unmarshal, opts...)
David Bainbridge86971522019-09-26 22:09:39 +000057}
58
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053059// UnmarshalStrict is similar to Unmarshal (please read its documentation for reference), with the following exceptions:
60//
61// - Duplicate fields in an object yield an error. This is according to the YAML specification.
62// - If obj, or any of its recursive children, is a struct, presence of fields in the serialized data unknown to the struct will yield an error.
63func UnmarshalStrict(yamlBytes []byte, obj interface{}, opts ...JSONOpt) error {
64 return unmarshal(yamlBytes, obj, yaml.UnmarshalStrict, append(opts, DisallowUnknownFields)...)
David Bainbridge86971522019-09-26 22:09:39 +000065}
66
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053067// unmarshal unmarshals the given YAML byte stream into the given interface,
David Bainbridge86971522019-09-26 22:09:39 +000068// optionally performing the unmarshalling strictly
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053069func unmarshal(yamlBytes []byte, obj interface{}, unmarshalFn func([]byte, interface{}) error, opts ...JSONOpt) error {
70 jsonTarget := reflect.ValueOf(obj)
71
72 jsonBytes, err := yamlToJSONTarget(yamlBytes, &jsonTarget, unmarshalFn)
Zack Williamse940c7a2019-08-21 14:25:39 -070073 if err != nil {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053074 return fmt.Errorf("error converting YAML to JSON: %w", err)
Zack Williamse940c7a2019-08-21 14:25:39 -070075 }
76
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053077 err = jsonUnmarshal(bytes.NewReader(jsonBytes), obj, opts...)
Zack Williamse940c7a2019-08-21 14:25:39 -070078 if err != nil {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053079 return fmt.Errorf("error unmarshaling JSON: %w", err)
Zack Williamse940c7a2019-08-21 14:25:39 -070080 }
81
82 return nil
83}
84
David Bainbridge86971522019-09-26 22:09:39 +000085// jsonUnmarshal unmarshals the JSON byte stream from the given reader into the
86// object, optionally applying decoder options prior to decoding. We are not
87// using json.Unmarshal directly as we want the chance to pass in non-default
88// options.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053089func jsonUnmarshal(reader io.Reader, obj interface{}, opts ...JSONOpt) error {
90 d := json.NewDecoder(reader)
David Bainbridge86971522019-09-26 22:09:39 +000091 for _, opt := range opts {
92 d = opt(d)
93 }
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053094 if err := d.Decode(&obj); err != nil {
David Bainbridge86971522019-09-26 22:09:39 +000095 return fmt.Errorf("while decoding JSON: %v", err)
96 }
97 return nil
98}
99
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530100// JSONToYAML converts JSON to YAML. Notable implementation details:
101//
102// - Duplicate fields, are case-sensitively ignored in an undefined order.
103// - The sequence indentation style is compact, which means that the "- " marker for a YAML sequence will be on the same indentation level as the sequence field name.
104// - Unlike Unmarshal, all integers, up to 64 bits, are preserved during this round-trip.
Zack Williamse940c7a2019-08-21 14:25:39 -0700105func JSONToYAML(j []byte) ([]byte, error) {
106 // Convert the JSON to an object.
107 var jsonObj interface{}
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530108
Zack Williamse940c7a2019-08-21 14:25:39 -0700109 // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the
110 // Go JSON library doesn't try to pick the right number type (int, float,
111 // etc.) when unmarshalling to interface{}, it just picks float64
112 // universally. go-yaml does go through the effort of picking the right
113 // number type, so we can preserve number type throughout this process.
114 err := yaml.Unmarshal(j, &jsonObj)
115 if err != nil {
116 return nil, err
117 }
118
119 // Marshal this object into YAML.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530120 yamlBytes, err := yaml.Marshal(jsonObj)
121 if err != nil {
122 return nil, err
123 }
124
125 return yamlBytes, nil
Zack Williamse940c7a2019-08-21 14:25:39 -0700126}
127
David Bainbridge86971522019-09-26 22:09:39 +0000128// YAMLToJSON converts YAML to JSON. Since JSON is a subset of YAML,
129// passing JSON through this method should be a no-op.
Zack Williamse940c7a2019-08-21 14:25:39 -0700130//
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530131// Some things YAML can do that are not supported by JSON:
132// - In YAML you can have binary and null keys in your maps. These are invalid
133// in JSON, and therefore int, bool and float keys are converted to strings implicitly.
134// - Binary data in YAML with the !!binary tag is not supported. If you want to
135// use binary data with this library, encode the data as base64 as usual but do
136// not use the !!binary tag in your YAML. This will ensure the original base64
137// encoded data makes it all the way through to the JSON.
138// - And more... read the YAML specification for more details.
David Bainbridge86971522019-09-26 22:09:39 +0000139//
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530140// Notable about the implementation:
141//
142// - Duplicate fields are case-sensitively ignored in an undefined order. Note that the YAML specification forbids duplicate fields, so this logic is more permissive than it needs to. See YAMLToJSONStrict for an alternative.
143// - As per the YAML 1.1 specification, which yaml.v2 used underneath implements, literal 'yes' and 'no' strings without quotation marks will be converted to true/false implicitly.
144// - Unlike Unmarshal, all integers, up to 64 bits, are preserved during this round-trip.
145// - There are no compatibility guarantees for returned error values.
Zack Williamse940c7a2019-08-21 14:25:39 -0700146func YAMLToJSON(y []byte) ([]byte, error) {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530147 return yamlToJSONTarget(y, nil, yaml.Unmarshal)
Zack Williamse940c7a2019-08-21 14:25:39 -0700148}
149
David Bainbridge86971522019-09-26 22:09:39 +0000150// YAMLToJSONStrict is like YAMLToJSON but enables strict YAML decoding,
151// returning an error on any duplicate field names.
152func YAMLToJSONStrict(y []byte) ([]byte, error) {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530153 return yamlToJSONTarget(y, nil, yaml.UnmarshalStrict)
David Bainbridge86971522019-09-26 22:09:39 +0000154}
155
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530156func yamlToJSONTarget(yamlBytes []byte, jsonTarget *reflect.Value, unmarshalFn func([]byte, interface{}) error) ([]byte, error) {
Zack Williamse940c7a2019-08-21 14:25:39 -0700157 // Convert the YAML to an object.
158 var yamlObj interface{}
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530159 err := unmarshalFn(yamlBytes, &yamlObj)
Zack Williamse940c7a2019-08-21 14:25:39 -0700160 if err != nil {
161 return nil, err
162 }
163
164 // YAML objects are not completely compatible with JSON objects (e.g. you
165 // can have non-string keys in YAML). So, convert the YAML-compatible object
166 // to a JSON-compatible object, failing with an error if irrecoverable
167 // incompatibilties happen along the way.
168 jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget)
169 if err != nil {
170 return nil, err
171 }
172
173 // Convert this object to JSON and return the data.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530174 jsonBytes, err := json.Marshal(jsonObj)
175 if err != nil {
176 return nil, err
177 }
178 return jsonBytes, nil
Zack Williamse940c7a2019-08-21 14:25:39 -0700179}
180
181func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (interface{}, error) {
182 var err error
183
184 // Resolve jsonTarget to a concrete value (i.e. not a pointer or an
185 // interface). We pass decodingNull as false because we're not actually
186 // decoding into the value, we're just checking if the ultimate target is a
187 // string.
188 if jsonTarget != nil {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530189 jsonUnmarshaler, textUnmarshaler, pointerValue := indirect(*jsonTarget, false)
Zack Williamse940c7a2019-08-21 14:25:39 -0700190 // We have a JSON or Text Umarshaler at this level, so we can't be trying
191 // to decode into a string.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530192 if jsonUnmarshaler != nil || textUnmarshaler != nil {
Zack Williamse940c7a2019-08-21 14:25:39 -0700193 jsonTarget = nil
194 } else {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530195 jsonTarget = &pointerValue
Zack Williamse940c7a2019-08-21 14:25:39 -0700196 }
197 }
198
199 // If yamlObj is a number or a boolean, check if jsonTarget is a string -
200 // if so, coerce. Else return normal.
201 // If yamlObj is a map or array, find the field that each key is
202 // unmarshaling to, and when you recurse pass the reflect.Value for that
203 // field back into this function.
204 switch typedYAMLObj := yamlObj.(type) {
205 case map[interface{}]interface{}:
206 // JSON does not support arbitrary keys in a map, so we must convert
207 // these keys to strings.
208 //
209 // From my reading of go-yaml v2 (specifically the resolve function),
210 // keys can only have the types string, int, int64, float64, binary
211 // (unsupported), or null (unsupported).
212 strMap := make(map[string]interface{})
213 for k, v := range typedYAMLObj {
214 // Resolve the key to a string first.
215 var keyString string
216 switch typedKey := k.(type) {
217 case string:
218 keyString = typedKey
219 case int:
220 keyString = strconv.Itoa(typedKey)
221 case int64:
222 // go-yaml will only return an int64 as a key if the system
223 // architecture is 32-bit and the key's value is between 32-bit
224 // and 64-bit. Otherwise the key type will simply be int.
225 keyString = strconv.FormatInt(typedKey, 10)
226 case float64:
227 // Stolen from go-yaml to use the same conversion to string as
228 // the go-yaml library uses to convert float to string when
229 // Marshaling.
230 s := strconv.FormatFloat(typedKey, 'g', -1, 32)
231 switch s {
232 case "+Inf":
233 s = ".inf"
234 case "-Inf":
235 s = "-.inf"
236 case "NaN":
237 s = ".nan"
238 }
239 keyString = s
240 case bool:
241 if typedKey {
242 keyString = "true"
243 } else {
244 keyString = "false"
245 }
246 default:
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530247 return nil, fmt.Errorf("unsupported map key of type: %s, key: %+#v, value: %+#v",
Zack Williamse940c7a2019-08-21 14:25:39 -0700248 reflect.TypeOf(k), k, v)
249 }
250
251 // jsonTarget should be a struct or a map. If it's a struct, find
252 // the field it's going to map to and pass its reflect.Value. If
253 // it's a map, find the element type of the map and pass the
254 // reflect.Value created from that type. If it's neither, just pass
255 // nil - JSON conversion will error for us if it's a real issue.
256 if jsonTarget != nil {
257 t := *jsonTarget
258 if t.Kind() == reflect.Struct {
259 keyBytes := []byte(keyString)
260 // Find the field that the JSON library would use.
261 var f *field
262 fields := cachedTypeFields(t.Type())
263 for i := range fields {
264 ff := &fields[i]
265 if bytes.Equal(ff.nameBytes, keyBytes) {
266 f = ff
267 break
268 }
269 // Do case-insensitive comparison.
270 if f == nil && ff.equalFold(ff.nameBytes, keyBytes) {
271 f = ff
272 }
273 }
274 if f != nil {
275 // Find the reflect.Value of the most preferential
276 // struct field.
277 jtf := t.Field(f.index[0])
278 strMap[keyString], err = convertToJSONableObject(v, &jtf)
279 if err != nil {
280 return nil, err
281 }
282 continue
283 }
284 } else if t.Kind() == reflect.Map {
285 // Create a zero value of the map's element type to use as
286 // the JSON target.
287 jtv := reflect.Zero(t.Type().Elem())
288 strMap[keyString], err = convertToJSONableObject(v, &jtv)
289 if err != nil {
290 return nil, err
291 }
292 continue
293 }
294 }
295 strMap[keyString], err = convertToJSONableObject(v, nil)
296 if err != nil {
297 return nil, err
298 }
299 }
300 return strMap, nil
301 case []interface{}:
302 // We need to recurse into arrays in case there are any
303 // map[interface{}]interface{}'s inside and to convert any
304 // numbers to strings.
305
306 // If jsonTarget is a slice (which it really should be), find the
307 // thing it's going to map to. If it's not a slice, just pass nil
308 // - JSON conversion will error for us if it's a real issue.
309 var jsonSliceElemValue *reflect.Value
310 if jsonTarget != nil {
311 t := *jsonTarget
312 if t.Kind() == reflect.Slice {
313 // By default slices point to nil, but we need a reflect.Value
314 // pointing to a value of the slice type, so we create one here.
315 ev := reflect.Indirect(reflect.New(t.Type().Elem()))
316 jsonSliceElemValue = &ev
317 }
318 }
319
320 // Make and use a new array.
321 arr := make([]interface{}, len(typedYAMLObj))
322 for i, v := range typedYAMLObj {
323 arr[i], err = convertToJSONableObject(v, jsonSliceElemValue)
324 if err != nil {
325 return nil, err
326 }
327 }
328 return arr, nil
329 default:
330 // If the target type is a string and the YAML type is a number,
331 // convert the YAML type to a string.
332 if jsonTarget != nil && (*jsonTarget).Kind() == reflect.String {
333 // Based on my reading of go-yaml, it may return int, int64,
334 // float64, or uint64.
335 var s string
336 switch typedVal := typedYAMLObj.(type) {
337 case int:
338 s = strconv.FormatInt(int64(typedVal), 10)
339 case int64:
340 s = strconv.FormatInt(typedVal, 10)
341 case float64:
342 s = strconv.FormatFloat(typedVal, 'g', -1, 32)
343 case uint64:
344 s = strconv.FormatUint(typedVal, 10)
345 case bool:
346 if typedVal {
347 s = "true"
348 } else {
349 s = "false"
350 }
351 }
352 if len(s) > 0 {
353 yamlObj = interface{}(s)
354 }
355 }
356 return yamlObj, nil
357 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700358}
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000359
360// JSONObjectToYAMLObject converts an in-memory JSON object into a YAML in-memory MapSlice,
361// without going through a byte representation. A nil or empty map[string]interface{} input is
362// converted to an empty map, i.e. yaml.MapSlice(nil).
363//
364// interface{} slices stay interface{} slices. map[string]interface{} becomes yaml.MapSlice.
365//
366// int64 and float64 are down casted following the logic of github.com/go-yaml/yaml:
367// - float64s are down-casted as far as possible without data-loss to int, int64, uint64.
368// - int64s are down-casted to int if possible without data-loss.
369//
370// Big int/int64/uint64 do not lose precision as in the json-yaml roundtripping case.
371//
372// string, bool and any other types are unchanged.
373func JSONObjectToYAMLObject(j map[string]interface{}) yaml.MapSlice {
374 if len(j) == 0 {
375 return nil
376 }
377 ret := make(yaml.MapSlice, 0, len(j))
378 for k, v := range j {
379 ret = append(ret, yaml.MapItem{Key: k, Value: jsonToYAMLValue(v)})
380 }
381 return ret
382}
383
384func jsonToYAMLValue(j interface{}) interface{} {
385 switch j := j.(type) {
386 case map[string]interface{}:
387 if j == nil {
388 return interface{}(nil)
389 }
390 return JSONObjectToYAMLObject(j)
391 case []interface{}:
392 if j == nil {
393 return interface{}(nil)
394 }
395 ret := make([]interface{}, len(j))
396 for i := range j {
397 ret[i] = jsonToYAMLValue(j[i])
398 }
399 return ret
400 case float64:
401 // replicate the logic in https://github.com/go-yaml/yaml/blob/51d6538a90f86fe93ac480b35f37b2be17fef232/resolve.go#L151
402 if i64 := int64(j); j == float64(i64) {
403 if i := int(i64); i64 == int64(i) {
404 return i
405 }
406 return i64
407 }
408 if ui64 := uint64(j); j == float64(ui64) {
409 return ui64
410 }
411 return j
412 case int64:
413 if i := int(j); j == int64(i) {
414 return i
415 }
416 return j
417 }
418 return j
419}