Search code examples
arraysjsongounmarshallingflickr

Unmarshal only gives me first element of array that is nested in JSON response


I am trying to work with the Flickr API and I have an endpoint which gives me a response like this

{
  "photos": {
    "page": 1,
    "pages": 500,
    "perpage": 1,
    "total": 500,
    "photo": [
      {
        "id": "12345",
        "owner": "12345@N01",
        "secret": "12ab345xyz",
        "server": "1234",
        "farm": 1,
        "title": "Some Title",
        "ispublic": 1,
        "isfriend": 0,
        "isfamily": 0
      },
     ...
    ]
  },
  "stat": "ok"
}

I have a struct that looks like this

type FlickrResp struct {
    Photos struct {
        Page int            `json:"page"`       //what page the results were retreived from
        Pages int           `json:"pages"`      //total number of pages of results
        Perpage int         `json:"perpage"`    //number of results per page
        Total int           `json:"total"`      //total number of results
        Photo []struct {
            ID string       `json:"id"`         //image ID
            Owner string    `json:"owner"`      //owner ID
            Secret string   `json:"secret"`     //owner secret
            Server string   `json:"server"`     //server ID
            Farm int        `json:"farm"`       //farm ID
            Title string    `json:"title"`      //image title
            Ispublic string `json:"ispublic"`   //is image publicly viewable?
            Isfriend string `json:"isfriend"`   //is image owner my friend?
            Isfamily string `json:"isfamily"`   //is image owner my family?
        }                   `json:"photo"`      //array of objects with details of photos
    }                       `json:"photos"`     //object that holds information about photoset
    Stat string             `json:"stat"`       //status
}

and I'm trying to Unmarshal thusly

var fr FlickrResp                                               //create blank struct of type FlickrResp to hold JSON
resp, err := ioutil.ReadAll(flickrCB.Body)                      //read HTTP GET response
if err != nil {

}
json.Unmarshal([]byte(resp), &fr)                               //serialize JSON into template

flickCB.Body comes from an http.Get

The problem is that I'm only getting the first element of the photo array. I can see that I have more than one element when I print the response object cast as a string. What am I doing wrong? I've been banging my head against Unmarshal and more complicated JSON responses for a few days now.


Solution

  • Answered originally in comments, but for a bit more clarification:

    There was an uncaught error from json.Unmarshal:

    json: cannot unmarshal number into Go value of type string

    Caused by a type mismatch between the flicker API giving ispublic, isfriend, and isfamily as integers, but the struct was expecting strings. An easy fix is just to convert them to ints.

    Another option for these sorts of situations is to use json.Number, which wraps a basic string from the source json and provides a few convenience methods to convert it to a number later (or just keep it as a string). By doing say

    Ispublic json.Number `json:"ispublic"`   //is image publicly viewable?
    Isfriend json.Number `json:"isfriend"`   //is image owner my friend?
    Isfamily json.Number `json:"isfamily"`
    

    It will work fine, and the result can easily be gotten as a string via fr.Ispublic.String().

    A more elaborate change that might be useful in this situation is to use a custom boolean type. The values in question seem to mirror booleans, but integers do not map cleanly to booleans either. A custom deserialized type can be used. Borrowing from this example elsewhere on stackoverflow:

    type ConvertibleBoolean bool
    
    func (bit ConvertibleBoolean) UnmarshalJSON(data []byte) error {
        asString := string(data)
        if asString == "1" || asString == "true" {
            bit = true
        } else if asString == "0" || asString == "false" {
            bit = false
        } else {
            return errors.New(fmt.Sprintf("Boolean unmarshal error: invalid input %s", asString))
        }
        return nil
    }
    

    The above would let you declare the Is* values as ConvertibleBoolean, and will automatically map the 0 & 1 values you'd presumably get in those fields from Flickr to booleans.