Parse JSON HTTP response using golang

Typically in these situations you will see people describe all of these sub struct types. If you don't actually need to reuse the definition of any sub structs (like as a type for a function argument), then you don't need to define them. You can just use one definition for the whole response. In addition, in some cases you don't need to define a type at all, you can just do it at the time of declaration:

package main
import "encoding/json"

const s = `
{  
   "subsets": [
      {  
         "addresses": [ 
            {"ip": "192.168.103.178"}
         ]
      }
   ]
}
`

func main() {
   var svc struct {
      Subsets []struct {
         Addresses []struct { Ip string }
      }
   }
   json.Unmarshal([]byte(s), &svc)
   ip := svc.Subsets[0].Addresses[0].Ip
   println(ip == "192.168.103.178")
}

One approach is to unmarshal the JSON to a map, e.g. (assumes jsData contains JSON string)

obj := map[string]interface{}{}
if err := json.Unmarshal([]byte(jsData), &obj); err != nil {
    log.Fatal(err)
}

Next, implement a function for searching the value associated with a key from the map recursively, e.g.

func find(obj interface{}, key string) (interface{}, bool) {
    //if the argument is not a map, ignore it
    mobj, ok := obj.(map[string]interface{})
    if !ok {
        return nil, false
    }

    for k, v := range mobj {
        //key match, return value
        if k == key {
            return v, true
        }

        //if the value is a map, search recursively
        if m, ok := v.(map[string]interface{}); ok {
            if res, ok := find(m, key); ok {
                return res, true
            }
        }
        //if the value is an array, search recursively 
        //from each element
        if va, ok := v.([]interface{}); ok {
            for _, a := range va {
                if res, ok := find(a, key); ok {
                    return res,true
                }
            }
        }
    }

    //element not found
    return nil,false
}

Note, that the above function return an interface{}. You need to convert it to appropriate type, e.g. using type switch:

if ip, ok := find(obj, "ip"); ok {
    switch v := ip.(type) {
    case string:
        fmt.Printf("IP is a string -> %s\n", v)
    case fmt.Stringer:
        fmt.Printf("IP implements stringer interface -> %s\n", v.String())
    case int:

    default:
        fmt.Printf("IP = %v, ok = %v\n", ip, ok)
    }
}

A working example can be found at https://play.golang.org/p/O5NUi4J0iR


You can write your own decoder or use existing third-party decoders. For instance, github.com/buger/jsonparser could solve your problem by iterating throw array (two times).

package main

import (
    "github.com/buger/jsonparser"
    "fmt"
)

var data =[]byte(`{
  "type":"example",
  "data":{
    "name":"abc",
    "labels":{
      "key":"value"
    }
  },
  "subsets":[
    {
      "addresses":[
        {
          "ip":"192.168.103.178"
        }
      ],
      "ports":[
        {
          "port":80
        }
      ]
    }
  ]
}`)

func main() {
    jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        jsonparser.ArrayEach(value, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
            v, _, _, err := jsonparser.Get(value, "ip")
            if err != nil {
                return
            }
            fmt.Println("ip: ", string(v[:]))
        }, "addresses")
    }, "subsets")
}

Output: ip: 192.168.103.178


You can create structs which reflect your json structure and then decode your json.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "log"
)

type Example struct {
    Type    string   `json:"type,omitempty"`
    Subsets []Subset `json:"subsets,omitempty"`
}

type Subset struct {
    Addresses []Address `json:"addresses,omitempty"`
}

type Address struct {
    IP string `json:"IP,omitempty"`
}

    func main() {

    m := []byte(`{"type":"example","data": {"name": "abc","labels": {"key": "value"}},"subsets": [{"addresses": [{"ip": "192.168.103.178"}],"ports": [{"port": 80}]}]}`)

    r := bytes.NewReader(m)
    decoder := json.NewDecoder(r)

    val := &Example{}
    err := decoder.Decode(val)

    if err != nil {
        log.Fatal(err)
    }

    // If you want to read a response body
    // decoder := json.NewDecoder(res.Body)
    // err := decoder.Decode(val)

    // Subsets is a slice so you must loop over it
    for _, s := range val.Subsets {
        // within Subsets, address is also a slice
        // then you can access each IP from type Address
        for _, a := range s.Addresses {
            fmt.Println(a.IP)
        }
    }

}

The output would be: 192.168.103.178

By decoding this to a struct, you can loop over any slice and not limit yourself to one IP

Example here:

https://play.golang.org/p/sWA9qBWljA

Tags:

Json

Go