介绍
支持 Json & Yaml 双向数据转换!
特性
- 支持保留字段顺序
- 支持保留原始类型
- 支持保留number/float类型精度
工具
# 1. 下载devtool
go install -v github.com/anthony-dong/golang/cli/devtool@master
# 2. yaml to json 转换
cat output/test.yaml | devtool codec yaml2json
# 3. json to yaml 转化
cat output/test.json | devtool codec json2yaml
代码实现
package utils
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"github.com/iancoleman/orderedmap"
"github.com/tidwall/gjson"
"gopkg.in/yaml.v3"
)
func JsonToYaml(input []byte) (output []byte, err error) {
yamlNode := &yaml.Node{}
if err := toYAMLNode(yamlNode, gjson.ParseBytes(input)); err != nil {
return nil, err
}
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
encoder.SetIndent(2)
if err := encoder.Encode(yamlNode); err != nil {
return nil, fmt.Errorf("error encoding YAML: %v", err)
}
return buf.Bytes(), nil
}
func toYAMLNode(node *yaml.Node, data gjson.Result) error {
switch data.Type {
case gjson.JSON:
var err error
if data.IsObject() {
node.Kind = yaml.MappingNode
data.ForEach(func(key, value gjson.Result) bool {
keyNode := &yaml.Node{}
if err = toYAMLNode(keyNode, key); err != nil {
return false
}
keyNode.Tag = ""
valNode := &yaml.Node{}
if err = toYAMLNode(valNode, value); err != nil {
return false
}
node.Content = append(node.Content, keyNode, valNode)
return true
})
if err != nil {
return err
}
} else if data.IsArray() {
node.Kind = yaml.SequenceNode
// todo style
data.ForEach(func(_, value gjson.Result) bool {
valNode := &yaml.Node{}
if err = toYAMLNode(valNode, value); err != nil {
return false
}
node.Content = append(node.Content, valNode)
return true
})
if err != nil {
return err
}
} else {
return fmt.Errorf("invalid JSON node type: %v", data.Type)
}
case gjson.Number:
node.Kind = yaml.ScalarNode
node.Value = data.Raw
case gjson.String:
node.Kind = yaml.ScalarNode
node.Tag = "!!str"
node.Value = data.Str
case gjson.False, gjson.True:
node.Kind = yaml.ScalarNode
node.Tag = "!!bool"
node.Value = data.String()
case gjson.Null:
node.Kind = yaml.ScalarNode
node.Tag = "!!null"
node.Value = "null"
default:
return fmt.Errorf("invalid JSON node type: %v", data.Type)
}
return nil
}
func YamlToJson(input []byte) ([]byte, error) {
yamlNode := &yaml.Node{}
if err := yaml.Unmarshal(input, yamlNode); err != nil {
return nil, err
}
node, err := toJsonNode(yamlNode)
if err != nil {
return nil, err
}
output := bytes.NewBuffer(nil)
encoder := json.NewEncoder(output)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
if err := encoder.Encode(node); err != nil {
return nil, err
}
return output.Bytes(), nil
}
func toJsonNode(node *yaml.Node) (interface{}, error) {
toString := func(input interface{}) string {
switch v := input.(type) {
case string:
return v
case json.Number:
return string(v)
case bool:
if v {
return "true"
}
return "false"
case nil:
return "null"
default:
return fmt.Sprintf(`%v`, input)
}
}
switch node.Kind {
case yaml.MappingNode:
result := orderedmap.New()
result.SetEscapeHTML(false)
for index, _ := range node.Content {
if index%2 == 1 {
key, err := toJsonNode(node.Content[index-1])
if err != nil {
return nil, err
}
value, err := toJsonNode(node.Content[index])
if err != nil {
return nil, err
}
result.Set(toString(key), value)
}
}
return result, nil
case yaml.SequenceNode:
result := make([]interface{}, 0, len(node.Content))
for index, _ := range node.Content {
value, err := toJsonNode(node.Content[index])
if err != nil {
return nil, err
}
result = append(result, value)
}
return result, nil
case yaml.ScalarNode:
switch node.Tag {
case "!!str":
return node.Value, nil
case "!!float", "!!int":
return json.Number(node.Value), nil
case "!!bool":
return strconv.ParseBool(node.Value)
case "!!null":
return nil, nil
}
return node.Value, nil
case yaml.DocumentNode:
if len(node.Content) == 1 {
return toJsonNode(node.Content[0])
}
clone := *node
clone.Kind = yaml.SequenceNode
return toJsonNode(&clone)
default:
return nil, fmt.Errorf("invalid JSON node type: %v", node.Kind)
}
}
测试用例
package utils
import (
"regexp"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_JsonToYaml_Obj(t *testing.T) {
jsonData := `
{
"str_data": "hello\nworld",
"escape": "echo ${FIRST_KEY} && echo ${ALIAS_ENV}",
"int_data": 132131231231313122312312312321111,
"arr_data": [
"Go",
"Python",
"JavaScript",
132131231231313122312312312321111,
null,
{
"k1": "v1",
"k2": [
1,
2,
3,
-1,
-1.1
]
}
],
"bool_data": true,
"null_data": null,
"11": 11,
"1.11": 1.11,
"1.111": "1.111",
"1.1111": "11",
"null": null,
"false": false,
"true": "true"
}
`
yamlData, err := JsonToYaml([]byte(jsonData))
if err != nil {
t.Fatal(err)
}
t.Log(string(yamlData))
output, err := YamlToJson(yamlData)
if err != nil {
t.Fatal(err)
}
t.Log(string(output))
assert.Equal(t, rmSpace(jsonData), rmSpace(string(output)))
}
func rmSpace(input string) string {
return regexp.MustCompile(`\s+`).ReplaceAllString(input, "")
}
func Test_JsonToYaml_Array(t *testing.T) {
jsonData := `["Go", "Python", "JavaScript"]`
yamlData, err := JsonToYaml([]byte(jsonData))
if err != nil {
t.Fatal(err)
}
t.Log(string(yamlData))
output, err := YamlToJson(yamlData)
if err != nil {
t.Fatal(err)
}
t.Log(string(output))
assert.Equal(t, rmSpace(jsonData), rmSpace(string(output)))
}