Home > Blockchain >  Idiomatic JSON object array -> map[string]json.RawMessage
Idiomatic JSON object array -> map[string]json.RawMessage

Time:06-09

I am trying to unmarshal the results of an HTTP REST API request that, depending on the number of results, returns either an object or an array of objects.

Requests are generic because I'm trying to build a wrapper around a specific REST API and it's called like:

// Function prototype where I am trying to convert the response
func (client *Client) Get(endpoint string, data map[string]string) (map[string]json.RawMessage, error)

// The way the function is called
client.Get("/v1/users", map[string]string{"filter.abc": "lorem ipsum"})

Two examples of response:

[
  {
    "abc": "def",
    "efg": 123
    "hij": [
      {
        "klm": "nop"
      }
    ]
  },
  {
    "abc": "def",
    "efg": 123
    "hij": [
      {
        "klm": "nop"
      }
    ]
  }
]
// RESPONSE 1: Array of JSON objects that have child arrays

{
  "abc": "def",
  "efg": 123
  "hij": [
    {
      "klm": "nop"
    }
  ]
}
// RESPONSE 2: In this case, only one element was returned.

I've achieved to do this for only the response 2 with something like this:

// [...]
byteBody = ioutil.ReadAll(res.Body)
// [...]
var body map[string]json.RawMessage
if err := json.Unmarshal(byteBody, &body); err != nil { [...] }

So, what's the most idiomatic way of parsing this? Is there any way to avoid writing redundant code and parse both responses? I was thinking in giving as an additional parameter the "model" the response should be put into. Is it a good practice? Thank you so much in advance!

CodePudding user response:

Not sure is idiomatic but this code could be an example.

In short you can try to unmarshal in one format and, if it fails, in the other one

the key function is

func parseStr(data string) ([]Item, error) {
    item := Item{}
    if err := json.Unmarshal([]byte(data), &item); err == nil {
        return []Item{item}, nil
    }

    items := []Item{}
    if err := json.Unmarshal([]byte(data), &items); err != nil {
        return nil, errors.New("invalid JSON data")
    }
    return items, nil

}

CodePudding user response:

Examine the first non-whitespace byte in the JSON document to determine if the document is an array or object.

func decodeArrayOrObject(doc []byte) ([]map[string]json.RawMessage, error) {
    doc = bytes.TrimSpace(doc)
    switch {
    case len(doc) == 0:
        return nil, errors.New("empty body")
    case doc[0] == '{':
        var m map[string]json.RawMessage
        err := json.Unmarshal(doc, &m)
        return []map[string]json.RawMessage{m}, err
    case doc[0] == '[':
        var s []map[string]json.RawMessage
        err := json.Unmarshal(doc, &s)
        return s, err
    default:
        return nil, errors.New("unexpected type")
    }
}

Use the reflect package to create a function that works with arbitrary slice types:

func decodeArrayOrObject(doc []byte, slicep interface{}) error {
    doc = bytes.TrimSpace(doc)
    switch {
    case len(doc) == 0:
        return errors.New("empty document")
    case doc[0] == '[':
        return json.Unmarshal(doc, slicep)
    case doc[0] == '{':
        s := reflect.ValueOf(slicep).Elem()
        s.Set(reflect.MakeSlice(s.Type(), 1, 1))
        return json.Unmarshal(doc, s.Index(0).Addr().Interface())
    default:
        return errors.New("unexpected type")
    }
}

Call the function with a pointer to a slice of structs or a pointer to a slice of struct pointers.

var v []Example
err := decodeArrayOrObject(body, &v)

Run an example on the playground.

  • Related