Convert JSON key value pair into single field value in Go

Issue

I have a json like following, where value can be int or string

{
   "data": [
       {
         "Name": "a_name",
         "value": 1
       },
       {
         "Name": "b_name",
         "value": "val"
       },
       {
         "Name": "c_name",
         "value": 2
       }
    ]
}

Now I want to convert that json into following struct, like only extract a_name and b_name value.

type Data struct {
   AName int `json: "a_name"`
   BName string `json: "b_name"`
}

I can do it by following way

import (
    "encoding/json"
    "fmt"
)

type TmpData struct {
    Data []struct {
        Name  string      `json:"Name"`
        Value interface{} `json:"value"`
    } `json:"data"`
}

type ExpectedData struct {
    AName int    `json: "a_name"`
    BName string `json: "b_name"`
}

func main() {
    data := `{
   "data": [
       {
         "Name": "a_name",
         "value": 1
       },
       {
         "Name": "b_name",
         "value": "val"
       },
       {
         "Name": "c_name",
         "value": 2
       }
    ]
}`
    tmpData := &TmpData{}
    json.Unmarshal([]byte(data), tmpData)

    ans := &ExpectedData{}
    for _, d := range tmpData.Data {
        if d.Name == "a_name" {
            ans.AName = int(d.Value.(float64))
        } else if d.Name == "b_name" {
            ans.BName = d.Value.(string)
        }
    }
    fmt.Println(ans)

}

Is there any better solution for this?

Solution

Not possible with the standard JSON un-marshalling unless you write a custom un-marshaller for your Data type.

The key here is to define the type for value to be an interface{}, so that multiple types could be stored in your b_name record.

func (d *Data) UnmarshalJSON(data []byte) error {
    var result Details
    if err := json.Unmarshal(data, &result); err != nil {
        return err
    }

    for _, value := range result.Data {
        switch value.Name {
        //  The json package will assume float64 when Unmarshalling with an interface{}
        case "a_name":
            v, ok := (value.Value).(float64)
            if !ok {
                return fmt.Errorf("a_name got data of type %T but wanted float64", value.Value)
            }
            d.AName = int(v)
        case "b_name":
            v, ok := (value.Value).(string)
            if !ok {
                return fmt.Errorf("b_name got data of type %T but wanted string", value.Value)
            }
            d.BName = v
        }
    }
    return nil
}

Playground – https://go.dev/play/p/GrXKAE87d1F

Answered By – Inian

Answer Checked By – David Goodson (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.