@@ -15,6 +15,7 @@ import (
1515 "io"
1616 "reflect"
1717 "strconv"
18+ "strings"
1819
1920 "gopkg.in/yaml.v3"
2021)
@@ -77,20 +78,78 @@ func jsonUnmarshal(r io.Reader, o interface{}, opts ...JSONOpt) error {
7778
7879// JSONToYAML converts JSON to YAML.
7980func JSONToYAML (j []byte ) ([]byte , error ) {
80- // Convert the JSON to an object.
81- var jsonObj interface {}
81+ var n yaml.Node
8282 // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the
8383 // Go JSON library doesn't try to pick the right number type (int, float,
8484 // etc.) when unmarshalling to interface{}, it just picks float64
8585 // universally. go-yaml does go through the effort of picking the right
8686 // number type, so we can preserve number type throughout this process.
87- err := yaml .Unmarshal (j , & jsonObj )
87+ err := yaml .Unmarshal (j , & n )
8888 if err != nil {
8989 return nil , err
9090 }
9191
92+ // Force yaml.Node to be marshaled as formatted YAML.
93+ enforceNodeStyle (& n )
94+
9295 // Marshal this object into YAML.
93- return yaml .Marshal (jsonObj )
96+ return yaml .Marshal (& n )
97+ }
98+
99+ func enforceNodeStyle (n * yaml.Node ) {
100+ if n == nil {
101+ return
102+ }
103+
104+ switch n .Kind {
105+ case yaml .SequenceNode , yaml .MappingNode :
106+ n .Style = yaml .LiteralStyle
107+ case yaml .ScalarNode :
108+ // Special case: if node is a string, then there are special styling
109+ // rules that we must abide by to conform to yaml.v3. Some of the logic
110+ // has been copied out, because the other way would've been to re-encode
111+ // the string which causes a ~2x performance hit!
112+ //
113+ // Ideally, we wouldn't need to copy this at all!
114+ // https://github.com/go-yaml/yaml/pull/574 implements a fix for this
115+ // issue that included code for the internal node() marshaling function,
116+ // except https://github.com/go-yaml/yaml/pull/583 was merged instead,
117+ // and it completely left out the fix for this issue!
118+ //
119+ // Instead of trying to make a pull request to a repository which hasn't
120+ // received any commit in over 2 years and has 125+ open pull requests,
121+ // I've decided to just copy the code here.
122+ //
123+ // There is one case that has been omitted from this code, though: the
124+ // code makes no attempt at checking for isBase64Float(). The README of
125+ // the YAML package says that it doesn't support this either (but it is
126+ // in the code).
127+ if n .ShortTag () == "!!str" {
128+ switch {
129+ case strings .Contains (n .Value , "\n " ):
130+ n .Style = yaml .LiteralStyle
131+ case isOldBool (n .Value ):
132+ n .Style = yaml .DoubleQuotedStyle
133+ default :
134+ n .Style = yaml .FlowStyle
135+ }
136+ }
137+ }
138+
139+ for _ , c := range n .Content {
140+ enforceNodeStyle (c )
141+ }
142+ }
143+
144+ // isOldBool is copied from yaml.v3.
145+ func isOldBool (s string ) (result bool ) {
146+ switch s {
147+ case "y" , "Y" , "yes" , "Yes" , "YES" , "on" , "On" , "ON" ,
148+ "n" , "N" , "no" , "No" , "NO" , "off" , "Off" , "OFF" :
149+ return true
150+ default :
151+ return false
152+ }
94153}
95154
96155// YAMLToJSON converts YAML to JSON. Since JSON is a subset of YAML,
@@ -109,9 +168,8 @@ func YAMLToJSON(y []byte) ([]byte, error) { //nolint:revive
109168}
110169
111170func yamlToJSON (dec * yaml.Decoder , jsonTarget * reflect.Value ) ([]byte , error ) {
112- // Convert the YAML to an object.
113- var yamlObj interface {}
114- if err := dec .Decode (& yamlObj ); err != nil {
171+ var n yaml.Node
172+ if err := dec .Decode (& n ); err != nil {
115173 // Functionality changed in v3 which means we need to ignore EOF error.
116174 // See https://github.com/go-yaml/yaml/issues/639
117175 if ! errors .Is (err , io .EOF ) {
@@ -123,7 +181,7 @@ func yamlToJSON(dec *yaml.Decoder, jsonTarget *reflect.Value) ([]byte, error) {
123181 // can have non-string keys in YAML). So, convert the YAML-compatible object
124182 // to a JSON-compatible object, failing with an error if irrecoverable
125183 // incompatibilities happen along the way.
126- jsonObj , err := convertToJSONableObject (yamlObj , jsonTarget )
184+ jsonObj , err := convertToJSONableObject (& n , jsonTarget )
127185 if err != nil {
128186 return nil , err
129187 }
@@ -132,7 +190,7 @@ func yamlToJSON(dec *yaml.Decoder, jsonTarget *reflect.Value) ([]byte, error) {
132190 return json .Marshal (jsonObj )
133191}
134192
135- func convertToJSONableObject (yamlObj interface {} , jsonTarget * reflect.Value ) (interface {} , error ) { //nolint:gocyclo
193+ func convertToJSONableObject (n * yaml. Node , jsonTarget * reflect.Value ) (json. RawMessage , error ) { //nolint:gocyclo
136194 var err error
137195
138196 // Resolve jsonTarget to a concrete value (i.e. not a pointer or an
@@ -150,57 +208,52 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
150208 }
151209 }
152210
153- // go-yaml v3 changed from v2 and now will provide map[string]interface{} by
154- // default and map[interface{}]interface{} when none of the keys strings.
155- // To get around this, we run a pre-loop to convert the map.
156- // JSON only supports strings as keys, so we must convert.
157-
158- switch typedYAMLObj := yamlObj .(type ) {
159- case map [interface {}]interface {}:
160- // From my reading of go-yaml v2 (specifically the resolve function),
161- // keys can only have the types string, int, int64, float64, binary
162- // (unsupported), or null (unsupported).
163- strMap := make (map [string ]interface {})
164- for k , v := range typedYAMLObj {
211+ switch n .Kind {
212+ case yaml .DocumentNode :
213+ return convertToJSONableObject (n .Content [0 ], jsonTarget )
214+
215+ case yaml .MappingNode :
216+ jsonMap := make (orderedMap , 0 , len (n .Content )/ 2 )
217+ keyNodes := make (map [string ]* yaml.Node , len (n .Content )/ 2 )
218+ for i := 0 ; i < len (n .Content ); i += 2 {
219+ kNode := n .Content [i ]
220+ vNode := n .Content [i + 1 ]
221+
222+ var anyKey interface {}
223+ if err := kNode .Decode (& anyKey ); err != nil {
224+ return nil , fmt .Errorf ("error decoding yaml map key %s: %v" , kNode .Tag , err )
225+ }
226+
165227 // Resolve the key to a string first.
166- var keyString string
167- switch typedKey := k .(type ) {
228+ var key string
229+ switch typedKey := anyKey .(type ) {
168230 case string :
169- keyString = typedKey
231+ key = typedKey
170232 case int :
171- keyString = strconv .Itoa (typedKey )
233+ key = strconv .Itoa (typedKey )
172234 case int64 :
173235 // go-yaml will only return an int64 as a key if the system
174236 // architecture is 32-bit and the key's value is between 32-bit
175237 // and 64-bit. Otherwise the key type will simply be int.
176- keyString = strconv .FormatInt (typedKey , 10 )
238+ key = strconv .FormatInt (typedKey , 10 )
177239 case float64 :
178240 // Float64 is now supported in keys
179- keyString = strconv .FormatFloat (typedKey , 'g' , - 1 , 64 )
241+ key = strconv .FormatFloat (typedKey , 'g' , - 1 , 64 )
180242 case bool :
181243 if typedKey {
182- keyString = "true"
244+ key = "true"
183245 } else {
184- keyString = "false"
246+ key = "false"
185247 }
186248 default :
187249 return nil , fmt .Errorf ("unsupported map key of type: %s, key: %+#v, value: %+#v" ,
188- reflect .TypeOf (k ), k , v )
250+ reflect .TypeOf (kNode ), kNode , vNode )
189251 }
190- strMap [keyString ] = v
191- }
192- // replace yamlObj with our new string map
193- yamlObj = strMap
194- }
195252
196- // If yamlObj is a number or a boolean, check if jsonTarget is a string -
197- // if so, coerce. Else return normal.
198- // If yamlObj is a map or array, find the field that each key is
199- // unmarshaling to, and when you recurse pass the reflect.Value for that
200- // field back into this function.
201- switch typedYAMLObj := yamlObj .(type ) {
202- case map [string ]interface {}:
203- for k , v := range typedYAMLObj {
253+ if otherNode , ok := keyNodes [key ]; ok {
254+ return nil , fmt .Errorf ("mapping key %q already defined at line %d" , key , otherNode .Line )
255+ }
256+ keyNodes [key ] = kNode
204257
205258 // jsonTarget should be a struct or a map. If it's a struct, find
206259 // the field it's going to map to and pass its reflect.Value. If
@@ -210,7 +263,7 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
210263 if jsonTarget != nil {
211264 t := * jsonTarget
212265 if t .Kind () == reflect .Struct {
213- keyBytes := []byte (k )
266+ keyBytes := []byte (key )
214267 // Find the field that the JSON library would use.
215268 var f * field
216269 fields := cachedTypeFields (t .Type ())
@@ -229,8 +282,7 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
229282 // Find the reflect.Value of the most preferential
230283 // struct field.
231284 jtf := t .Field (f .index [0 ])
232- typedYAMLObj [k ], err = convertToJSONableObject (v , & jtf )
233- if err != nil {
285+ if err := jsonMap .AppendYAML (f .name , vNode , & jtf ); err != nil {
234286 return nil , err
235287 }
236288 continue
@@ -239,20 +291,21 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
239291 // Create a zero value of the map's element type to use as
240292 // the JSON target.
241293 jtv := reflect .Zero (t .Type ().Elem ())
242- typedYAMLObj [k ], err = convertToJSONableObject (v , & jtv )
243- if err != nil {
294+ if err := jsonMap .AppendYAML (key , vNode , & jtv ); err != nil {
244295 return nil , err
245296 }
246297 continue
247298 }
248299 }
249- typedYAMLObj [ k ], err = convertToJSONableObject ( v , nil )
250- if err != nil {
300+
301+ if err := jsonMap . AppendYAML ( key , vNode , nil ); err != nil {
251302 return nil , err
252303 }
253304 }
254- return typedYAMLObj , nil
255- case []interface {}:
305+
306+ return jsonMap .MarshalJSON ()
307+
308+ case yaml .SequenceNode :
256309 // We need to recurse into arrays in case there are any
257310 // map[interface{}]interface{}'s inside and to convert any
258311 // numbers to strings.
@@ -272,22 +325,28 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
272325 }
273326
274327 // Make and use a new array.
275- arr := make ([]interface {} , len (typedYAMLObj ))
276- for i , v := range typedYAMLObj {
328+ arr := make ([]json. RawMessage , len (n . Content ))
329+ for i , v := range n . Content {
277330 arr [i ], err = convertToJSONableObject (v , jsonSliceElemValue )
278331 if err != nil {
279332 return nil , err
280333 }
281334 }
282- return arr , nil
335+ return json .Marshal (arr )
336+
283337 default :
338+ var rawObject interface {}
339+ if err := n .Decode (& rawObject ); err != nil {
340+ return nil , fmt .Errorf ("error decoding yaml object %s: %v" , n .Tag , err )
341+ }
342+
284343 // If the target type is a string and the YAML type is a number,
285344 // convert the YAML type to a string.
286345 if jsonTarget != nil && (* jsonTarget ).Kind () == reflect .String {
287346 // Based on my reading of go-yaml, it may return int, int64,
288347 // float64, or uint64.
289348 var s string
290- switch typedVal := typedYAMLObj .(type ) {
349+ switch typedVal := rawObject .(type ) {
291350 case int :
292351 s = strconv .FormatInt (int64 (typedVal ), 10 )
293352 case int64 :
@@ -304,9 +363,49 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
304363 }
305364 }
306365 if len (s ) > 0 {
307- yamlObj = interface {}( s )
366+ rawObject = s
308367 }
309368 }
310- return yamlObj , nil
369+
370+ return json .Marshal (rawObject )
371+ }
372+ }
373+
374+ type orderedMap []orderedPair
375+
376+ type orderedPair struct {
377+ K string
378+ V interface {}
379+ }
380+
381+ func (m * orderedMap ) AppendYAML (k string , v * yaml.Node , jsonTarget * reflect.Value ) error {
382+ r , err := convertToJSONableObject (v , jsonTarget )
383+ if err != nil {
384+ return fmt .Errorf ("%q: %w" , k , err )
385+ }
386+ * m = append (* m , orderedPair {K : k , V : r })
387+ return nil
388+ }
389+
390+ func (m orderedMap ) MarshalJSON () ([]byte , error ) {
391+ var buf bytes.Buffer
392+ buf .WriteByte ('{' )
393+ for i , p := range m {
394+ if i > 0 {
395+ buf .WriteByte (',' )
396+ }
397+ k , err := json .Marshal (p .K )
398+ if err != nil {
399+ return nil , fmt .Errorf ("key %q error: %w" , p .K , err )
400+ }
401+ buf .Write (k )
402+ buf .WriteByte (':' )
403+ b , err := json .Marshal (p .V )
404+ if err != nil {
405+ return nil , fmt .Errorf ("value %q error: %w" , p .K , err )
406+ }
407+ buf .Write (b )
311408 }
409+ buf .WriteByte ('}' )
410+ return buf .Bytes (), nil
312411}
0 commit comments