sontags sontags - 2 days ago 5
YAML Question

Passing values to interface{}

Short

The following code does not exactly do what expected:
https://play.golang.org/p/sO4w4I_Lle

I assume that I mess up some pointer/reference stuff as usual, however I expect my...

func unmarshalJSON(in []byte, s interface{}) error


... and
encoding/json
s...

func Unmarshal(data []byte, v interface{}) error


...to behave the same way (eg. update the referenced passed as second argument).

Long

The example above is a minimal reproducer that does not make much sense. This is in order to make it work on the playground. However, an less minimal example that does make sense is this:

package main

import (
"fmt"

"gopkg.in/yaml.v2"
)

func unmarshalYAML(in []byte, s interface{}) error {
var result map[interface{}]interface{}
err := yaml.Unmarshal(in, &result)
s = cleanUpInterfaceMap(result)
// s is printed as expected
fmt.Println(s) // map[aoeu:[test aoeu] oaeu:[map[mahl:aoec tase:aoeu]]]
return err
}

func cleanUpInterfaceArray(in []interface{}) []interface{} {
out := make([]interface{}, len(in))
for i, v := range in {
out[i] = cleanUpMapValue(v)
}
return out
}

func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
out := make(map[string]interface{})
for k, v := range in {
out[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
}
return out
}

func cleanUpMapValue(v interface{}) interface{} {
switch v := v.(type) {
case []interface{}:
return cleanUpInterfaceArray(v)
case map[interface{}]interface{}:
return cleanUpInterfaceMap(v)
case string:
return v
default:
return fmt.Sprintf("%v", v)
}
}

func main() {
s := make(map[string]interface{})
b := []byte(`---
aoeu:
- test
- aoeu
oaeu:
- { tase: aoeu, mahl: aoec}
`)
err := unmarshalYAML(b, &s)
if err != nil {
panic(err)
}
// s is still an empty map
fmt.Println(s) // map[]
}


The idea is to unmarshal YAML to
map[string]interface{}
(instead of
map[interface{}]interface{}
) is order to allow to serialize to JSON (where identifiers need to be strings). The
unmarshalYAML
function should provide the same func signture as
yaml.Unmarshal
...

Answer

Using Type assertion

Inside your unmarshalJSON() function the parameter s behaves like a local variable. When you assign something to it:

s = result

It will only change the value of the local variable.

Since you want it to work with changing the value of a *map[string]interface{} and that is what you pass to it, you could use a simple type assertion to obtain the map pointer from it, and pass this pointer to json.Unmarshal():

func unmarshalJSON(in []byte, s interface{}) error {
    if m, ok := s.(*map[string]interface{}); !ok {
        return errors.New("Expecting *map[string]interface{}")
    } else {
        return json.Unmarshal(in, m)
    }
}

Try your modified, working example on the Go Playground.

Just passing it along

Also note that however this is completely unnecessary as json.Unmarshal() is also defined to take the destination as a value of type interface{}, the same thing you have. So you don't even have to do anything just pass it along:

func unmarshalJSON(in []byte, s interface{}) error {
    return json.Unmarshal(in, s)
}

Try this on the Go Playground.

With a variable of function type

As an interesting thing note that the signature of your unmarshalJSON() and the library function json.Unmarshal() is identical:

// Yours:
func unmarshalJSON(in []byte, s interface{}) error

// json package
func Unmarshal(data []byte, v interface{}) error

This means there is another option, that is you could use a variable named unmarshalJSON of a function type, and just simply assign the function value json.Unmarshal:

var unmarshalJSON func([]byte, interface{}) error = json.Unmarshal

Now you have a variable unmarshalJSON which is of function type, and you can call it as if it would be a function:

err := unmarshalJSON(b, &s)

Try this function value on the Go Playground.

Now on to your unmarshalYAML() function

In your unmarshalYAML() you do the same mistake:

s = cleanUpInterfaceMap(result)

This will only change the value of your local s variable (parameter), and it will not "populate" the map (pointer) passed to unmarshalYAML().

Use the type assertion technique detailed above to obtain the pointer from the s interface{} argument, and once you have that, you can change the pointed object (the "outside" map).

func unmarshalYAML(in []byte, s interface{}) error {
    var dest *map[string]interface{}
    var ok bool
    if dest, ok = s.(*map[string]interface{}); !ok {
        return errors.New("Expecting *map[string]interface{}")
    }

    var result map[interface{}]interface{}
    if err := yaml.Unmarshal(in, &result); err != nil {
        return err
    }
    m := cleanUpInterfaceMap(result)

    // m holds the results, dest is the pointer that was passed to us,
    // we can just set the pointed object (map):
    *dest = m
    return nil
}
Comments