Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON Unmarshalling with different keys

Tags:

json

struct

go

I'm trying to unmarshal some JSON from different sources that may have different keys. For instance, I may have:

{
    "a": 1,
    "b": 2
}

Or I may have:

{
    "c": 1,
    "b": 2
}

In this case, I can guarantee that "b" will be there. However, I want "a" and "c" to be represented the same way. In effect, what I want is:

type MyJson struct {
    Init int `json:"a",json:"c"`
    Sec  int `json:"b"
}

Basically I want the unmarshaller to look for either key and set it as Init. Now this doesn't actually work (or I wouldn't be posting). Unmarshalling the first gives me what I want, while the second sets Init to 0. My ideal option would be to unmarshal to a struct, with one of two possibilities:

  1. I represent the struct with multiple tags as above
  2. I define multiple structs, but am able to choose which to unmarshal to

I tried to implement number 2 by making two different structs and a map, but it seems I can't make a map with a type as the second value:

type MyJson1 struct {
    Init int `json:"a"`
    Sec  int `json:"b"`
}

type MyJson2 struct {
    Init int `json:"c"`
    Sec  int `json:"b"`
}

Is there a way to define a set of structs that behaves like an interface? That is, they all have the same fields, but are defined differently. Or maybe there's another way. I could make the fields unmarshalling "a" and "c" other fields, then set Init accordingly. But this doesn't scale beyond a couple of variants.

Thanks!

like image 547
pswaminathan Avatar asked Feb 10 '23 20:02

pswaminathan


2 Answers

One possibility is to define a struct which has a field for all the variants for your possible inputs, and for convenience provide a method for this struct which will return the field that was found in the input:

type MyJson struct {
    A *int `json:"a"`
    C *int `json:"c"`

    Sec int `json:"b"`
}

func (j *MyJson) Init() int {
    if j.A == nil {
        return *j.C
    }
    return *j.A
}

Using it:

inputs := []string{
    `{"a": 1, "b": 2}`,
    `{"c": 1, "b": 2}`}

for _, input := range inputs {
    var my MyJson
    if err := json.Unmarshal([]byte(input), &my); err != nil {
        panic(err)
    }
    fmt.Printf("Init: %v, Sec: %v\n", my.Init(), my.Sec)
}

Output, as expected (try it on the Go Playground):

Init: 1, Sec: 2
Init: 1, Sec: 2

And a Little Trick:

In the original struct we added 2 fields for the 2 possible variants. I defined them as pointers so we can detect which one was found in the JSON input. Now if before unmarshalling we set these pointers to point to the same value, that's all we need: doesn't matter which variant of the input JSON we use, the same value will be set in memory, so you can always just read/refer the Init struct field:

type MyJson struct {
    Init *int `json:"a"`
    Init2 *int `json:"c"`

    Sec int `json:"b"`
}

func main() {
    inputs := []string{
        `{"a": 1, "b": 2}`,
        `{"c": 1, "b": 2}`}

    for _, input := range inputs {
        var my MyJson
        my.Init = new(int) // Allocate an int
        my.Init2 = my.Init // Set Init2 to point to the same value

        if err := json.Unmarshal([]byte(input), &my); err != nil {
            panic(err)
        }
        fmt.Printf("Init: %v, Sec: %v\n", *my.Init, my.Sec)
    }
}

Try it on the Go Playground.

You can create a function which creates and sets up your MyJson ready for unmarshalling like this:

func NewMyJson() (my MyJson) {
    my.Init = new(int) // Allocate an int
    my.Init2 = my.Init // Set Init2 to point to the same value
    return
}

And so using it becomes this simple:

var my = NewMyJson()
err := json.Unmarshal([]byte(input), &my)

Another Trick on the Tricked variant:

You can specify the Init field not to be a pointer because it is enough for Init2 to be a pointer and point to Init, so this becomes even more simple and desirable (but then NewMyJson must return a pointer):

type MyJson struct {
    Init  int `json:"a"`
    Init2 *int `json:"c"`

    Sec int `json:"b"`
}

func NewMyJson() *MyJson {
    my := new(MyJson)
    my.Init2 = &my.Init // Set Init2 to point to Init
    return my
}
like image 124
icza Avatar answered Feb 13 '23 22:02

icza


icza's approach is good, and better if this implementation could match stdlib's interface.

I suggest implementing json.Unmarshaler:

// it can't get c itself.
type MyJson struct {
    A int `json:"a"`
    B int `json:"b"`
}

func (j *MyJson) UnmarshalJSON(b []byte) error {
    type Alias MyJson
    realValue := struct {
        *Alias
        C int `json:"c"`  // <- now it can accept 'c' value
    }{(*Alias)(j), 0}

    if err := json.Unmarshal(b, &realValue); err != nil {
        return err
    } else if realValue.C != 0 {
        // if C has value, overwrite A
        realValue.A = realValue.C
    }

    return nil
}

And just decode it using json.Unmarshal.

like image 22
Jaemin Park Avatar answered Feb 13 '23 21:02

Jaemin Park