2

Here is an example (see also https://play.golang.org/p/or7z4Xc8tN):

package main

import (
    "encoding/json"
    "fmt"
)

type A struct {
    X string
    Y int
}

type B struct {
    A
    Y string 
}

func main() {
    data := []byte(`{"X": "me", "Y": "hi"}`)
    b := &B{}
    json.Unmarshal(data, b)
    fmt.Println(b)
    fmt.Println(b.A)

    b = &B{}
    data = []byte(`{"X": "me", "Y": 123}`)
    json.Unmarshal(data, b)
    fmt.Println(b)
    fmt.Println(b.A)
}

Which outputs:

&{{me 0} hi}
{me 0}
&{{me 0} }
{me 0}

Is there a way to polymorphically unmarshal the field Y to either an int or a string? Or even unmarshal into A.Y at all since B.Y is defined?

I know some might suggest unmarshalling with something like json.Unmarshall(data, &b.A), but I don't know if I can fit that into my current design.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
crunk1
  • 2,560
  • 1
  • 26
  • 32

2 Answers2

4

Go's only polymorphism is interfaces. Embedding does not offer polymorphism.

If you're trying to unmarshal JSON where you can't assume what type one of the fields is going to be, you can use a field of type interface{} along with type assertions, fmt.Sprint, or reflection. Which you should use depends on the particular use case - once you've got the value, what are you going to do with it? At some point you have to care if it's an int or a string, which will determine how you handle the value.

Adrian
  • 42,911
  • 6
  • 107
  • 99
2

As pointed by Adrian, Go does not support polymorphism through struct embedding. interface{} is the only way to hold any type of golang variable. However, in your case you can implement custom Unmarshaler to decode the JSON stream to a struct utilizing json.Number or interface{}. Below is implementation using json.Number. For more generic interface{} version, you can implement it as suggested by Adrian.

func (b *B) UnmarshalJSON(js []byte) error {
    //First: decode stream to anonymous struct
    v := struct {
        X string
        Y json.Number
    }{}

    err := json.Unmarshal(js, &v)
    if err != nil {
        return err
    }

    //Depends on the v.Y value, choose the holder variable
    //If it's convertible to number, assign to A.Y
    //otherwise, assign it to b.Y
    b.X = v.X
    if fv, err := v.Y.Float64(); err == nil {
        b.A.Y = int(fv)
    } else {
        b.Y = v.Y.String()
    }

    return nil
}

Working example can be found in The Go Playground.

putu
  • 6,218
  • 1
  • 21
  • 30
  • As written, the else is never executed. The return err after the call to err := json.Unmarshal(js, &v) causes the if/else not to occur when the data contents is not numeric (e.g., "hi"). With it present, the else (b.Y = v.Y.String()) never occurs. Additionally, the assignment of b.X doesn't happen. In the playground example, "me" is missing in the output because of this. Still, the example is helpful, so thanks @putu. – Software Prophets Apr 10 '21 at 14:28