0
votes

I want to Marshal and Unmarshal a binary tree like structure in Go. Each node corresponds to a struct of type Node. Nodes are interconnected through pointers (left and right children), much like in a linked list. The tree's leaves carry content which is implemented as an interface. All leaves of a tree have the same type of Content which is known to the unmarshaler beforehand.
I know that when Unmarshaling a struct with an interface in one field (say "Content"), I have to do a type assertion like
err = json.Unmarshal(byteSlice, &decodedStruct{Content: &MyStruct{}})
However, as the tree is of arbitrary size, my structs are deeply nested. Is there a straightforward/idiomatic way of Marshaling/Unmarshaling such an object I am not aware of?

Below, I post a minimal example which I believe represents the two key features of first, a sequence of pointers and second, an interface at the "end". (playground: https://play.golang.org/p/t9C9Hn4ONlE )

// LinkedList is a simple linked list defined by a root node
type LinkedList struct {
    Name string
    Root *Node
}

// Node is a list's node with Content
type Node struct {
    Child *Node
    C     Content
}

// Content is a dummy interface
type Content interface {
    CalculateSum() int
}

// MyStruct implements Content
type MyStruct struct {
    ID     int
    Values []int
}

// CalculateSum computes the sum of the slice in the field @Values
func (ms MyStruct) CalculateSum() (s int) {
    for _, i := range ms.Values {
        s += i
    }
    return
}

func main() {
    // Make a list of three nodes with content in the leaf
    ms := MyStruct{2, []int{2, 4, 7}}
    leaf := Node{nil, ms}
    node := Node{&leaf, nil}
    rootNode := Node{&node, nil}
    ll := LinkedList{"list1", &rootNode}

    // Encoding linked list works fine...
    llEncoded, err := json.Marshal(ll)

    // ...decoding doesn't:
    // error decoding:  json: cannot unmarshal object into Go struct field Node.Root.Child.Child.C of type main.Content
    llDecoded := LinkedList{}
    err = json.Unmarshal(llEncoded, &llDecoded)
    fmt.Println("error decoding: ", err)
}
1
Is every Content a MyStruct? If not, if you have more types implementing Content, how then do you know which leaf should be which type? And how do you plan on "telling" that to the unmarshaler? - mkopriva
@mkopriva: Thanks for the question. I should have mentioned that and amended the post accordingly. - jppade
@jppade: ?? I didn't ask a question. - Flimzy
@jppade Are you saying that the concrete value in Node.C is always a *MyStruct? - Bayta Darell
@MuffinTop No, it can be anything that implements Content. But I know what type it is when marshaling/unmarshaling. - jppade

1 Answers

0
votes

If you know the Content's concrete type upfront you can implement the json.Unmarshaler interface, unmarshaling into the hard-coded concrete type and then assign the result to the interface type.

func (n *Node) UnmarshalJSON(data []byte) error {
    var node struct {
        Child *Node
        C     *MyStruct
    }
    if err := json.Unmarshal(data, &node); err != nil {
        return err
    }
    n.Child = node.Child
    n.C = node.C
    return nil
}

https://play.golang.org/p/QOJuiLpYrze


If you need it to be more flexible you'll need to somehow tell the json.Unmarshaler implementation what concrete type the json represents. One way you could do this is to embed type information into the content's json, for example (now with the help of the json.Marshaler interface):

func (ms MyStruct) MarshalJSON() ([]byte, error) {
    type _MyStruct MyStruct

    var out = struct {
        Type string `json:"_type"`
        _MyStruct
    }{
        Type:      "MyStruct",
        _MyStruct: _MyStruct(ms),
    }
    return json.Marshal(out)
}

Update the Node's unmarshaler implementation accordingly:

func (n *Node) UnmarshalJSON(data []byte) error {
    var node struct {
        Child *Node
        C     json.RawMessage
    }
    if err := json.Unmarshal(data, &node); err != nil {
        return err
    }
    n.Child = node.Child

    if len(node.C) > 0 && string(node.C) != `null` {
        var _type struct {
            Type string `json:"_type"`
        }
        if err := json.Unmarshal([]byte(node.C), &_type); err != nil {
            return err
        }

        c := newContent[_type.Type]()
        if err := json.Unmarshal([]byte(node.C), c); err != nil {
            return err
        }
        n.C = c
    }
    return nil
}

and define the newContent as a map whose values are functions that return new instances of the concrete type:

var newContent = map[string]func() Content{
    "MyStruct": func() Content { return new(MyStruct) },
    // ...
}

Try it on playground: https://play.golang.org/p/u9L0VxEG4dT