Unmarshal dynamic interface using UnmarshalBSON based on known key

1.4k Views Asked by At

I have an object of type foo containing an interface ActivationInterface ; this object is saved in MongoDB and I have trouble fetching it back as the underlying type of the inner object is not known.

I implemented UnmarshalBSON as follow without success, as it seems even after setting the concrete type of the interface, the unmarshaller still does now the underlying type, as I still get the error: error decoding key act: no decoder found for main.ActivationInterface

Do you have any idea how I can achieve this ?

I found somethign close woking here, so I don't get why mine isn't: Unmarshal dynamic JSON based on a type key I cannot see what I'm doing wrong and different...!

EDIT: I updated the code to compare with json. UnmarshalJSON is working great with exactly the same code while UnmarshalBSON still fails.

package main

import (
    "fmt"
    "log"

    "go.mongodb.org/mongo-driver/bson"
)

type foo struct {
    Type string `bson:"type"`
    Act  ActivationInterface
}

type ActivationInterface interface{}

type Activation1 struct {
    Name string `bson:"name"`
}
type Activation2 struct {
    Address string `bson:"adress"`
}

func (q *foo) UnmarshalBSON(data []byte) error {
    // Unmarshall only the type
    fooTemp := new(struct {
        Type string `bson:"type"`
    })
    if err := bson.Unmarshal(data, fooTemp); err != nil {
        return err
    }

    fmt.Println(fooTemp.Type)

    // Set the type to the prop
    switch fooTemp.Type {
    case "act1":
        // q.Act = &Activation1{}
        q.Act = new(Activation1)
    case "act2":
        // q.Act = &Activation2{}
        q.Act = new(Activation2)
    default:
        fmt.Println("DEFAULT")
    }

    // Call Unmarshal again
    type Alias foo // avoids infinite recursion using a type alias
    return bson.Unmarshal(data, (*Alias)(q))
}

func main() {
    foo1 := foo{
        Type: "act1",
        Act: Activation1{
            Name: "name: act1",
        },
    }
    foo2 := foo{
        Type: "act2",
        Act: Activation2{
            Address: "adress: act2",
        },
    }

    // Marshal
    m1, err := bson.Marshal(foo1)
    if err != nil {
        log.Fatal(err)
    }
    m2, err := bson.Marshal(foo2)
    if err != nil {
        log.Fatal(err)
    }
    //fmt.Println(m1, m2)

    // Unmarshal
    var u1, u2 foo
    err = bson.Unmarshal(m1, &u1)
    if err != nil {
        fmt.Println("1 -> ", err) // error decoding key act: no decoder found for main.ActivationInterface
    }
    err = bson.Unmarshal(m2, &u2)
    if err != nil {
        fmt.Println("2 -> ", err) // error decoding key act: no decoder found for main.ActivationInterface
    }
    fmt.Println(foo1.Type, ":", u1.Act.(*Activation1).Name)
    fmt.Println(foo2.Type, ":", u2.Act.(*Activation2).Address)
}


Go playground: https://go.dev/play/p/bHMy6-ZLsYQ
Almost the same code, but using JSON and working: https://go.dev/play/p/V5HLrQ_-ls3

Thanks !

2

There are 2 best solutions below

0
On

When using an interface in your struct, unmarshall cannot figure out which "implementation" to choose... You have to do it manually, in your case based on the "type" field. Usually unmarshall methods put a map for interface{}, a key value store. Anyway back on your problem, you have to store the interface data in bson.Raw (slice of bytes) and do the unmarshalling manually by choosing the right struct.

package main

import (
    "fmt"
    "log"

    "go.mongodb.org/mongo-driver/bson"
)

type foo struct {
    Type string `bson:"type"`
    Act  ActivationInterface
}

type ActivationInterface interface{}

type Activation1 struct {
    Name string `bson:"name"`
}
type Activation2 struct {
    Address string `bson:"adress"`
}

func (q *foo) UnmarshalBSON(data []byte) error {
    // Unmarshall only the type
    fooTemp := new(struct {
        Type string `bson:"type"`
        Act  bson.Raw
    })
    if err := bson.Unmarshal(data, fooTemp); err != nil {
        return err
    }

    fmt.Println(fooTemp.Type)

    // Set the type to the prop
    switch fooTemp.Type {
    case "act1":
        // q.Act = &Activation1{}
        a := Activation1{}
        err := bson.Unmarshal(fooTemp.Act, &a)
        if err != nil {
            return err
        }
        q.Act = a
    case "act2":
        // q.Act = &Activation2{}
        a := Activation2{}
        err := bson.Unmarshal(fooTemp.Act, &a)
        if err != nil {
            return err
        }
        q.Act = a
    default:
        fmt.Println("DEFAULT")
        return fmt.Errorf("unknown type: %v", fooTemp.Type)
    }

    return nil
}

func main() {
    foo1 := foo{
        Type: "act1",
        Act: Activation1{
            Name: "name: act1",
        },
    }
    foo2 := foo{
        Type: "act2",
        Act: Activation2{
            Address: "adress: act2",
        },
    }

    // Marshal
    m1, err := bson.Marshal(foo1)
    if err != nil {
        log.Fatal(err)
    }
    m2, err := bson.Marshal(foo2)
    if err != nil {
        log.Fatal(err)
    }
    //fmt.Println(m1, m2)

    // Unmarshal
    var u1, u2 foo
    err = bson.Unmarshal(m1, &u1)
    if err != nil {
        fmt.Println("1 -> ", err) // error decoding key act: no decoder found for main.ActivationInterface
    }
    err = bson.Unmarshal(m2, &u2)
    if err != nil {
        fmt.Println("2 -> ", err) // error decoding key act: no decoder found for main.ActivationInterface
    }
    fmt.Println(foo1.Type, ":", u1.Act.(Activation1).Name)
    fmt.Println(foo2.Type, ":", u2.Act.(Activation2).Address)
}

https://go.dev/play/p/CG2SlEknNrO

0
On

The way i achieved it is also with implementing the UnmarshalBSON interface.

Translated to your example it would be:

type foo struct {
    Type string              `bson:"type"`
    Act  ActivationInterface `bson:"act,omitempty"`
}

func (f foo) UnmarshalBSON(data []byte) error {

    var raw map[string]interface{}
    err := bson.Unmarshal(data, &raw)
    if err != nil {
        return err
    }

    f.Type = raw["type"].(string)

    switch f.Type {
    case "Activation1":
        var act Activation1
        actBytes, err := bson.Marshal(raw["act"])
        if err != nil {
            return err
        }
        err = bson.Unmarshal(actBytes, &act)
        if err != nil {
            return err
        }
        f.Act = act
    case "Activation2":
        var act Activation2
        actBytes, err := bson.Marshal(raw["act"])
        if err != nil {
            return err
        }
        err = bson.Unmarshal(actBytes, &act)
        if err != nil {
            return err
        }
        f.Act = act
    }

    return nil

}

type ActivationInterface interface {
    // Placeholder TODO: Replace with useful function
    Placeholder()
}

type Activation1 struct {
    Name string `bson:"name"`
}

func (a Activation1) Placeholder() {

}

type Activation2 struct {
    Address string `bson:"adress"`
}

func (a Activation2) Placeholder() {

}