1package yaml 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "reflect" 8 "strconv" 9 10 "gopkg.in/yaml.v2" 11) 12 13// Marshals the object into JSON then converts JSON to YAML and returns the 14// YAML. 15func Marshal(o interface{}) ([]byte, error) { 16 j, err := json.Marshal(o) 17 if err != nil { 18 return nil, fmt.Errorf("error marshaling into JSON: %v", err) 19 } 20 21 y, err := JSONToYAML(j) 22 if err != nil { 23 return nil, fmt.Errorf("error converting JSON to YAML: %v", err) 24 } 25 26 return y, nil 27} 28 29// Converts YAML to JSON then uses JSON to unmarshal into an object. 30func Unmarshal(y []byte, o interface{}) error { 31 vo := reflect.ValueOf(o) 32 j, err := yamlToJSON(y, &vo) 33 if err != nil { 34 return fmt.Errorf("error converting YAML to JSON: %v", err) 35 } 36 37 err = json.Unmarshal(j, o) 38 if err != nil { 39 return fmt.Errorf("error unmarshaling JSON: %v", err) 40 } 41 42 return nil 43} 44 45// Convert JSON to YAML. 46func JSONToYAML(j []byte) ([]byte, error) { 47 // Convert the JSON to an object. 48 var jsonObj interface{} 49 // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the 50 // Go JSON library doesn't try to pick the right number type (int, float, 51 // etc.) when unmarshalling to interface{}, it just picks float64 52 // universally. go-yaml does go through the effort of picking the right 53 // number type, so we can preserve number type throughout this process. 54 err := yaml.Unmarshal(j, &jsonObj) 55 if err != nil { 56 return nil, err 57 } 58 59 // Marshal this object into YAML. 60 return yaml.Marshal(jsonObj) 61} 62 63// Convert YAML to JSON. Since JSON is a subset of YAML, passing JSON through 64// this method should be a no-op. 65// 66// Things YAML can do that are not supported by JSON: 67// * In YAML you can have binary and null keys in your maps. These are invalid 68// in JSON. (int and float keys are converted to strings.) 69// * Binary data in YAML with the !!binary tag is not supported. If you want to 70// use binary data with this library, encode the data as base64 as usual but do 71// not use the !!binary tag in your YAML. This will ensure the original base64 72// encoded data makes it all the way through to the JSON. 73func YAMLToJSON(y []byte) ([]byte, error) { 74 return yamlToJSON(y, nil) 75} 76 77func yamlToJSON(y []byte, jsonTarget *reflect.Value) ([]byte, error) { 78 // Convert the YAML to an object. 79 var yamlObj interface{} 80 err := yaml.Unmarshal(y, &yamlObj) 81 if err != nil { 82 return nil, err 83 } 84 85 // YAML objects are not completely compatible with JSON objects (e.g. you 86 // can have non-string keys in YAML). So, convert the YAML-compatible object 87 // to a JSON-compatible object, failing with an error if irrecoverable 88 // incompatibilties happen along the way. 89 jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget) 90 if err != nil { 91 return nil, err 92 } 93 94 // Convert this object to JSON and return the data. 95 return json.Marshal(jsonObj) 96} 97 98func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (interface{}, error) { 99 var err error 100 101 // Resolve jsonTarget to a concrete value (i.e. not a pointer or an 102 // interface). We pass decodingNull as false because we're not actually 103 // decoding into the value, we're just checking if the ultimate target is a 104 // string. 105 if jsonTarget != nil { 106 ju, tu, pv := indirect(*jsonTarget, false) 107 // We have a JSON or Text Umarshaler at this level, so we can't be trying 108 // to decode into a string. 109 if ju != nil || tu != nil { 110 jsonTarget = nil 111 } else { 112 jsonTarget = &pv 113 } 114 } 115 116 // If yamlObj is a number or a boolean, check if jsonTarget is a string - 117 // if so, coerce. Else return normal. 118 // If yamlObj is a map or array, find the field that each key is 119 // unmarshaling to, and when you recurse pass the reflect.Value for that 120 // field back into this function. 121 switch typedYAMLObj := yamlObj.(type) { 122 case map[interface{}]interface{}: 123 // JSON does not support arbitrary keys in a map, so we must convert 124 // these keys to strings. 125 // 126 // From my reading of go-yaml v2 (specifically the resolve function), 127 // keys can only have the types string, int, int64, float64, binary 128 // (unsupported), or null (unsupported). 129 strMap := make(map[string]interface{}) 130 for k, v := range typedYAMLObj { 131 // Resolve the key to a string first. 132 var keyString string 133 switch typedKey := k.(type) { 134 case string: 135 keyString = typedKey 136 case int: 137 keyString = strconv.Itoa(typedKey) 138 case int64: 139 // go-yaml will only return an int64 as a key if the system 140 // architecture is 32-bit and the key's value is between 32-bit 141 // and 64-bit. Otherwise the key type will simply be int. 142 keyString = strconv.FormatInt(typedKey, 10) 143 case float64: 144 // Stolen from go-yaml to use the same conversion to string as 145 // the go-yaml library uses to convert float to string when 146 // Marshaling. 147 s := strconv.FormatFloat(typedKey, 'g', -1, 32) 148 switch s { 149 case "+Inf": 150 s = ".inf" 151 case "-Inf": 152 s = "-.inf" 153 case "NaN": 154 s = ".nan" 155 } 156 keyString = s 157 case bool: 158 if typedKey { 159 keyString = "true" 160 } else { 161 keyString = "false" 162 } 163 default: 164 return nil, fmt.Errorf("Unsupported map key of type: %s, key: %+#v, value: %+#v", 165 reflect.TypeOf(k), k, v) 166 } 167 168 // jsonTarget should be a struct or a map. If it's a struct, find 169 // the field it's going to map to and pass its reflect.Value. If 170 // it's a map, find the element type of the map and pass the 171 // reflect.Value created from that type. If it's neither, just pass 172 // nil - JSON conversion will error for us if it's a real issue. 173 if jsonTarget != nil { 174 t := *jsonTarget 175 if t.Kind() == reflect.Struct { 176 keyBytes := []byte(keyString) 177 // Find the field that the JSON library would use. 178 var f *field 179 fields := cachedTypeFields(t.Type()) 180 for i := range fields { 181 ff := &fields[i] 182 if bytes.Equal(ff.nameBytes, keyBytes) { 183 f = ff 184 break 185 } 186 // Do case-insensitive comparison. 187 if f == nil && ff.equalFold(ff.nameBytes, keyBytes) { 188 f = ff 189 } 190 } 191 if f != nil { 192 // Find the reflect.Value of the most preferential 193 // struct field. 194 jtf := t.Field(f.index[0]) 195 strMap[keyString], err = convertToJSONableObject(v, &jtf) 196 if err != nil { 197 return nil, err 198 } 199 continue 200 } 201 } else if t.Kind() == reflect.Map { 202 // Create a zero value of the map's element type to use as 203 // the JSON target. 204 jtv := reflect.Zero(t.Type().Elem()) 205 strMap[keyString], err = convertToJSONableObject(v, &jtv) 206 if err != nil { 207 return nil, err 208 } 209 continue 210 } 211 } 212 strMap[keyString], err = convertToJSONableObject(v, nil) 213 if err != nil { 214 return nil, err 215 } 216 } 217 return strMap, nil 218 case []interface{}: 219 // We need to recurse into arrays in case there are any 220 // map[interface{}]interface{}'s inside and to convert any 221 // numbers to strings. 222 223 // If jsonTarget is a slice (which it really should be), find the 224 // thing it's going to map to. If it's not a slice, just pass nil 225 // - JSON conversion will error for us if it's a real issue. 226 var jsonSliceElemValue *reflect.Value 227 if jsonTarget != nil { 228 t := *jsonTarget 229 if t.Kind() == reflect.Slice { 230 // By default slices point to nil, but we need a reflect.Value 231 // pointing to a value of the slice type, so we create one here. 232 ev := reflect.Indirect(reflect.New(t.Type().Elem())) 233 jsonSliceElemValue = &ev 234 } 235 } 236 237 // Make and use a new array. 238 arr := make([]interface{}, len(typedYAMLObj)) 239 for i, v := range typedYAMLObj { 240 arr[i], err = convertToJSONableObject(v, jsonSliceElemValue) 241 if err != nil { 242 return nil, err 243 } 244 } 245 return arr, nil 246 default: 247 // If the target type is a string and the YAML type is a number, 248 // convert the YAML type to a string. 249 if jsonTarget != nil && (*jsonTarget).Kind() == reflect.String { 250 // Based on my reading of go-yaml, it may return int, int64, 251 // float64, or uint64. 252 var s string 253 switch typedVal := typedYAMLObj.(type) { 254 case int: 255 s = strconv.FormatInt(int64(typedVal), 10) 256 case int64: 257 s = strconv.FormatInt(typedVal, 10) 258 case float64: 259 s = strconv.FormatFloat(typedVal, 'g', -1, 32) 260 case uint64: 261 s = strconv.FormatUint(typedVal, 10) 262 case bool: 263 if typedVal { 264 s = "true" 265 } else { 266 s = "false" 267 } 268 } 269 if len(s) > 0 { 270 yamlObj = interface{}(s) 271 } 272 } 273 return yamlObj, nil 274 } 275 276 return nil, nil 277} 278