Unmarshal JSON object into slice of structs with key and value

Issue

I am still learning GO and I am stumped by the following problem:
I receive a JSON string that I want to unmarshal in GO.
The JSON looks like this

{
  "MAINKEY": {
    "key1": 1,
    "key2": [1, 2]
  }
}

I am only interested in the contents of the MAINKEY, but similar to this question, I do not know the names of the keys, which should reflect the names of the map.

In the end, I want to have the following object:

type Result struct {
    Key   string
    Value []int
}

expectedResult := []Result{
    {"key1", []int{1}},
    {"key2", []int{1, 2}},
}
fmt.Printf("WANT: %+v\n", expectedResult)
//> WANT: [{Key:key1 Value:[1]} {Key:key2 Value:[1 2]}]

If possible, I don’t want to unmarshal into a map[string]interface{} first (but if there is no other way, that would be ok too).

Full code so far is:

package main

import (
    "encoding/json"
    "fmt"
)

// does produce expected result at the moment...
type Result struct {
    Key   string
    Value []int
}

type Outer struct {
    Key Result `json:"MAINKEY"`
}

func main() {

    input := `{"MAINKEY": {"key1": 1, "key2": [1, 2]}}`

    var cont Outer
    json.Unmarshal([]byte(input), &cont)
    fmt.Printf("GOT: %+v\n", cont)

    expectedResult := []Result{
        {"key1", []int{1}},
        {"key2", []int{1, 2}},
    }
    fmt.Printf("WANT: %+v\n", expectedResult)
}

Solution

You can use a custom unmarshaler with a map:

type ResultList []Result

func (ls *ResultList) UnmarshalJSON(data []byte) error {
    var obj map[string]json.RawMessage
    if err := json.Unmarshal(data, &obj); err != nil {
        return err
    }

    for key, raw := range obj {
        r := Result{Key: key}
        if raw[0] == '[' { // assume array of ints
            if err := json.Unmarshal(raw, &r.Value); err != nil {
                return err
            }
        } else { // assume single int
            var i int
            if err := json.Unmarshal(raw, &i); err != nil {
                return err
            }
            r.Value = append(r.Value, i)
        }
        *ls = append(*ls, r)
    }
    return nil
}

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


Or, if you need to retain the order, you can tokenize the input:

type ResultList []Result

func (ls *ResultList) UnmarshalJSON(data []byte) error {
    d := json.NewDecoder(bytes.NewReader(data))
    i := -1
    for {
        t, err := d.Token()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }

        switch v := t.(type) {
        case string:
            *ls = append(*ls, Result{Key: v})
            i += 1
        case float64:
            (*ls)[i].Value = append((*ls)[i].Value, int(v))
        }
    }
    return nil
}

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

Answered By – mkopriva

Answer Checked By – Willingham (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.