package yaml import ( "bytes" "encoding/json" "fmt" "io" "reflect" "strconv" "gopkg.in/yaml.v2" ) // Marshals the object into JSON then converts JSON to YAML and returns the // YAML. func Marshal(o interface{}) ([]byte, error) { j, err := json.Marshal(o) if err != nil { return nil, fmt.Errorf("error marshaling into JSON: %v", err) } y, err := JSONToYAML(j) if err != nil { return nil, fmt.Errorf("error converting JSON to YAML: %v", err) } return y, nil } // JSONOpt is a decoding option for decoding from JSON format. type JSONOpt func(*json.Decoder) *json.Decoder // Unmarshal converts YAML to JSON then uses JSON to unmarshal into an object, // optionally configuring the behavior of the JSON unmarshal. func Unmarshal(y []byte, o interface{}, opts ...JSONOpt) error { vo := reflect.ValueOf(o) j, err := yamlToJSON(y, &vo) if err != nil { return fmt.Errorf("error converting YAML to JSON: %v", err) } err = jsonUnmarshal(bytes.NewReader(j), o, opts...) if err != nil { return fmt.Errorf("error unmarshaling JSON: %v", err) } return nil } // jsonUnmarshal unmarshals the JSON byte stream from the given reader into the // object, optionally applying decoder options prior to decoding. We are not // using json.Unmarshal directly as we want the chance to pass in non-default // options. func jsonUnmarshal(r io.Reader, o interface{}, opts ...JSONOpt) error { d := json.NewDecoder(r) for _, opt := range opts { d = opt(d) } if err := d.Decode(&o); err != nil { return fmt.Errorf("while decoding JSON: %v", err) } return nil } // Convert JSON to YAML. func JSONToYAML(j []byte) ([]byte, error) { // Convert the JSON to an object. var jsonObj interface{} // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the // Go JSON library doesn't try to pick the right number type (int, float, // etc.) when unmarshalling to interface{}, it just picks float64 // universally. go-yaml does go through the effort of picking the right // number type, so we can preserve number type throughout this process. err := yaml.Unmarshal(j, &jsonObj) if err != nil { return nil, err } // Marshal this object into YAML. return yaml.Marshal(jsonObj) } // Convert YAML to JSON. Since JSON is a subset of YAML, passing JSON through // this method should be a no-op. // // Things YAML can do that are not supported by JSON: // * In YAML you can have binary and null keys in your maps. These are invalid // in JSON. (int and float keys are converted to strings.) // * Binary data in YAML with the !!binary tag is not supported. If you want to // use binary data with this library, encode the data as base64 as usual but do // not use the !!binary tag in your YAML. This will ensure the original base64 // encoded data makes it all the way through to the JSON. func YAMLToJSON(y []byte) ([]byte, error) { return yamlToJSON(y, nil) } func yamlToJSON(y []byte, jsonTarget *reflect.Value) ([]byte, error) { // Convert the YAML to an object. var yamlObj interface{} err := yaml.Unmarshal(y, &yamlObj) if err != nil { return nil, err } // YAML objects are not completely compatible with JSON objects (e.g. you // can have non-string keys in YAML). So, convert the YAML-compatible object // to a JSON-compatible object, failing with an error if irrecoverable // incompatibilties happen along the way. jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget) if err != nil { return nil, err } // Convert this object to JSON and return the data. return json.Marshal(jsonObj) } func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (interface{}, error) { var err error // Resolve jsonTarget to a concrete value (i.e. not a pointer or an // interface). We pass decodingNull as false because we're not actually // decoding into the value, we're just checking if the ultimate target is a // string. if jsonTarget != nil { ju, tu, pv := indirect(*jsonTarget, false) // We have a JSON or Text Umarshaler at this level, so we can't be trying // to decode into a string. if ju != nil || tu != nil { jsonTarget = nil } else { jsonTarget = &pv } } // If yamlObj is a number or a boolean, check if jsonTarget is a string - // if so, coerce. Else return normal. // If yamlObj is a map or array, find the field that each key is // unmarshaling to, and when you recurse pass the reflect.Value for that // field back into this function. switch typedYAMLObj := yamlObj.(type) { case map[interface{}]interface{}: // JSON does not support arbitrary keys in a map, so we must convert // these keys to strings. // // From my reading of go-yaml v2 (specifically the resolve function), // keys can only have the types string, int, int64, float64, binary // (unsupported), or null (unsupported). strMap := make(map[string]interface{}) for k, v := range typedYAMLObj { // Resolve the key to a string first. var keyString string switch typedKey := k.(type) { case string: keyString = typedKey case int: keyString = strconv.Itoa(typedKey) case int64: // go-yaml will only return an int64 as a key if the system // architecture is 32-bit and the key's value is between 32-bit // and 64-bit. Otherwise the key type will simply be int. keyString = strconv.FormatInt(typedKey, 10) case float64: // Stolen from go-yaml to use the same conversion to string as // the go-yaml library uses to convert float to string when // Marshaling. s := strconv.FormatFloat(typedKey, 'g', -1, 32) switch s { case "+Inf": s = ".inf" case "-Inf": s = "-.inf" case "NaN": s = ".nan" } keyString = s case bool: if typedKey { keyString = "true" } else { keyString = "false" } default: return nil, fmt.Errorf("Unsupported map key of type: %s, key: %+#v, value: %+#v", reflect.TypeOf(k), k, v) } // jsonTarget should be a struct or a map. If it's a struct, find // the field it's going to map to and pass its reflect.Value. If // it's a map, find the element type of the map and pass the // reflect.Value created from that type. If it's neither, just pass // nil - JSON conversion will error for us if it's a real issue. if jsonTarget != nil { t := *jsonTarget if t.Kind() == reflect.Struct { keyBytes := []byte(keyString) // Find the field that the JSON library would use. var f *field fields := cachedTypeFields(t.Type()) for i := range fields { ff := &fields[i] if bytes.Equal(ff.nameBytes, keyBytes) { f = ff break } // Do case-insensitive comparison. if f == nil && ff.equalFold(ff.nameBytes, keyBytes) { f = ff } } if f != nil { // Find the reflect.Value of the most preferential // struct field. jtf := t.Field(f.index[0]) strMap[keyString], err = convertToJSONableObject(v, &jtf) if err != nil { return nil, err } continue } } else if t.Kind() == reflect.Map { // Create a zero value of the map's element type to use as // the JSON target. jtv := reflect.Zero(t.Type().Elem()) strMap[keyString], err = convertToJSONableObject(v, &jtv) if err != nil { return nil, err } continue } } strMap[keyString], err = convertToJSONableObject(v, nil) if err != nil { return nil, err } } return strMap, nil case []interface{}: // We need to recurse into arrays in case there are any // map[interface{}]interface{}'s inside and to convert any // numbers to strings. // If jsonTarget is a slice (which it really should be), find the // thing it's going to map to. If it's not a slice, just pass nil // - JSON conversion will error for us if it's a real issue. var jsonSliceElemValue *reflect.Value if jsonTarget != nil { t := *jsonTarget if t.Kind() == reflect.Slice { // By default slices point to nil, but we need a reflect.Value // pointing to a value of the slice type, so we create one here. ev := reflect.Indirect(reflect.New(t.Type().Elem())) jsonSliceElemValue = &ev } } // Make and use a new array. arr := make([]interface{}, len(typedYAMLObj)) for i, v := range typedYAMLObj { arr[i], err = convertToJSONableObject(v, jsonSliceElemValue) if err != nil { return nil, err } } return arr, nil default: // If the target type is a string and the YAML type is a number, // convert the YAML type to a string. if jsonTarget != nil && (*jsonTarget).Kind() == reflect.String { // Based on my reading of go-yaml, it may return int, int64, // float64, or uint64. var s string switch typedVal := typedYAMLObj.(type) { case int: s = strconv.FormatInt(int64(typedVal), 10) case int64: s = strconv.FormatInt(typedVal, 10) case float64: s = strconv.FormatFloat(typedVal, 'g', -1, 32) case uint64: s = strconv.FormatUint(typedVal, 10) case bool: if typedVal { s = "true" } else { s = "false" } } if len(s) > 0 { yamlObj = interface{}(s) } } return yamlObj, nil } return nil, nil }