Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert yaml to json without struct

Tags:

json

yaml

go

Services: 
-   Orders: 
    -   ID: $save ID1
        SupplierOrderCode: $SupplierOrderCode
    -   ID: $save ID2
        SupplierOrderCode: 111111

I want to convert this yaml string to json, cause the source data is dynamic, so I can't map it to a struct:

var body interface{}
err := yaml.Unmarshal([]byte(s), &body)

Then I want to convert that interface to json string again:

b, _ := json.Marshal(body)

But an error occur:

panic: json: unsupported type: map[interface {}]interface {}
like image 236
huynhminhtuan Avatar asked Nov 22 '16 08:11

huynhminhtuan


2 Answers

Foreword: I optimized and improved the below solution, and released it as a library here: github.com/icza/dyno. The below convert() function is available as dyno.ConvertMapI2MapS().


The problem is that if you use the most generic interface{} type to unmarshal into, the default type used by the github.com/go-yaml/yaml package to unmarshal key-value pairs will be map[interface{}]interface{}.

First idea would be to use map[string]interface{}:

var body map[string]interface{}

But this attempt falls short if the depth of the yaml config is more than one, as this body map will contain additional maps whose type will again be map[interface{}]interface{}.

The problem is that the depth is unknown, and there may be other values than maps, so using map[string]map[string]interface{} is not good.

A viable approach is to let yaml unmarshal into a value of type interface{}, and go through the result recursively, and convert each encountered map[interface{}]interface{} to a map[string]interface{} value. Both maps and slices have to be handled.

Here's an example of this converter function:

func convert(i interface{}) interface{} {
    switch x := i.(type) {
    case map[interface{}]interface{}:
        m2 := map[string]interface{}{}
        for k, v := range x {
            m2[k.(string)] = convert(v)
        }
        return m2
    case []interface{}:
        for i, v := range x {
            x[i] = convert(v)
        }
    }
    return i
}

And using it:

func main() {
    fmt.Printf("Input: %s\n", s)
    var body interface{}
    if err := yaml.Unmarshal([]byte(s), &body); err != nil {
        panic(err)
    }

    body = convert(body)

    if b, err := json.Marshal(body); err != nil {
        panic(err)
    } else {
        fmt.Printf("Output: %s\n", b)
    }
}

const s = `Services:
-   Orders:
    -   ID: $save ID1
        SupplierOrderCode: $SupplierOrderCode
    -   ID: $save ID2
        SupplierOrderCode: 111111
`

Output:

Input: Services:
-   Orders:
    -   ID: $save ID1
        SupplierOrderCode: $SupplierOrderCode
    -   ID: $save ID2
        SupplierOrderCode: 111111

Output: {"Services":[{"Orders":[
    {"ID":"$save ID1","SupplierOrderCode":"$SupplierOrderCode"},
    {"ID":"$save ID2","SupplierOrderCode":111111}]}]}

One thing to note: by switching from yaml to JSON via Go maps you'll lose the order of the items, as elements (key-value pairs) in a Go map are not ordered. This may or may not be a problem.

like image 101
icza Avatar answered Oct 13 '22 18:10

icza


http://sigs.k8s.io/yaml is "a wrapper around go-yaml designed to enable a better way of handling YAML when marshaling to and from structs". Among other things, it provides yaml.YAMLToJSON method that should do what you want.

like image 21
Lauri Peltonen Avatar answered Oct 13 '22 20:10

Lauri Peltonen