0

I found some posts on how to decoding json nested objects in go, I tried to apply the answers to my problem, but I only managed to find a partial solution.

My json file look like this:

{
"user":{
    "gender":"male",
    "age":"21-30",
    "id":"80b1ea88-19d7-24e8-52cc-65cf6fb9b380"
    },
"trials":{
    "0":{"index":0,"word":"WORD 1","Time":3000,"keyboard":true,"train":true,"type":"A"},
    "1":{"index":1,"word":"WORD 2","Time":3000,"keyboard":true,"train":true,"type":"A"},
    },
"answers":{
    "training":[
        {"ans":0,"RT":null,"gtAns":"WORD 1","correct":0},
        {"ans":0,"RT":null,"gtAns":"WORD 2","correct":0}
        ],
    "test":[
        {"ans":0,"RT":null,"gtAns":true,"correct":0},
        {"ans":0,"RT":null,"gtAns":true,"correct":0}
        ]
    }
}

Basically I need to parse the information inside it and save them into go structure. With the code below I managed to extract the user information, but it looks too complicated to me and it won't be easy to apply the same thing to the "answers" fields which contains 2 arrays with more than 100 entries each. Here the code I'm using now:

type userDetails struct {
    Id     string `json:"id"`
    Age    string `json:"age"`
    Gender string `json:"gender"`
}

type jsonRawData map[string]interface {
}

func getJsonContent(r *http.Request) ( userDetails) {
    defer r.Body.Close()
    jsonBody, err := ioutil.ReadAll(r.Body)
    var userDataCurr userDetails
    if err != nil {
        log.Printf("Couldn't read request body: %s", err)
    } else {
        var f jsonRawData
        err := json.Unmarshal(jsonBody, &f)
        if err != nil {
            log.Printf("Error unmashalling: %s", err)
        } else {
            user := f["user"].(map[string]interface{})
            userDataCurr.Id = user["id"].(string)
            userDataCurr.Gender = user["gender"].(string)
            userDataCurr.Age = user["age"].(string)
        }
    }
    return userDataCurr
}

Any suggestions? Thanks a lot!

1 Answer 1

3

You're doing it the hard way by using interface{} and not taking advantage of what encoding/json gives you.

I'd do it something like this (note I assumed there was an error with the type of the "gtAns" field and I made it a boolean, you don't give enough information to know what to do with the "RT" field):

package main

import (
        "encoding/json"
        "fmt"
        "io"
        "log"
        "strconv"
        "strings"
)

const input = `{
"user":{
    "gender":"male",
    "age":"21-30",
    "id":"80b1ea88-19d7-24e8-52cc-65cf6fb9b380"
    },
"trials":{
    "0":{"index":0,"word":"WORD 1","Time":3000,"keyboard":true,"train":true,"type":"A"},
    "1":{"index":1,"word":"WORD 2","Time":3000,"keyboard":true,"train":true,"type":"A"}
    },
"answers":{
    "training":[
        {"ans":0,"RT":null,"gtAns":true,"correct":0},
        {"ans":0,"RT":null,"gtAns":true,"correct":0}
        ],
    "test":[
        {"ans":0,"RT":null,"gtAns":true,"correct":0},
        {"ans":0,"RT":null,"gtAns":true,"correct":0}
        ]
    }
}`

type Whatever struct {
        User struct {
                Gender Gender   `json:"gender"`
                Age    Range    `json:"age"`
                ID     IDString `json:"id"`
        } `json:"user"`
        Trials map[string]struct {
                Index int    `json:"index"`
                Word  string `json:"word"`
                Time  int    // should this be a time.Duration?
                Train bool   `json:"train"`
                Type  string `json:"type"`
        } `json:"trials"`
        Answers map[string][]struct {
                Answer    int             `json:"ans"`
                RT        json.RawMessage // ??? what type is this
                GotAnswer bool            `json:"gtAns"`
                Correct   int             `json:"correct"`
        } `json:"answers"`
}

// Using some custom types to show custom marshalling:

type IDString string // TODO custom unmarshal and format/error checking

type Gender int

const (
        Male Gender = iota
        Female
)

func (g *Gender) UnmarshalJSON(b []byte) error {
        var s string
        err := json.Unmarshal(b, &s)
        if err != nil {
                return err
        }
        switch strings.ToLower(s) {
        case "male":
                *g = Male
        case "female":
                *g = Female
        default:
                return fmt.Errorf("invalid gender %q", s)
        }
        return nil
}
func (g Gender) MarshalJSON() ([]byte, error) {
        switch g {
        case Male:
                return []byte(`"male"`), nil
        case Female:
                return []byte(`"female"`), nil
        default:
                return nil, fmt.Errorf("invalid gender %v", g)
        }
}

type Range struct{ Min, Max int }

func (r *Range) UnmarshalJSON(b []byte) error {
        // XXX could be improved
        _, err := fmt.Sscanf(string(b), `"%d-%d"`, &r.Min, &r.Max)
        return err
}
func (r Range) MarshalJSON() ([]byte, error) {
        return []byte(fmt.Sprintf(`"%d-%d"`, r.Min, r.Max)), nil
        // Or:
        b := make([]byte, 0, 8)
        b = append(b, '"')
        b = strconv.AppendInt(b, int64(r.Min), 10)
        b = append(b, '-')
        b = strconv.AppendInt(b, int64(r.Max), 10)
        b = append(b, '"')
        return b, nil
}

func fromJSON(r io.Reader) (Whatever, error) {
        var x Whatever
        dec := json.NewDecoder(r)
        err := dec.Decode(&x)
        return x, err
}

func main() {
        // Use http.Get or whatever to get an io.Reader,
        // (e.g. response.Body).
        // For playground, substitute a fixed string
        r := strings.NewReader(input)

        // If you actually had a string or []byte:
        //     var x Whatever
        //     err := json.Unmarshal([]byte(input), &x)

        x, err := fromJSON(r)
        if err != nil {
                log.Fatal(err)
        }
        fmt.Println(x)
        fmt.Printf("%+v\n", x)

        b, err := json.MarshalIndent(x, "", "  ")
        if err != nil {
                log.Fatal(err)
        }
        fmt.Printf("Re-marshalled: %s\n", b)

}

Playground

Of course if you want to reuse those sub-types you could pull them out of the "Whatever" type into their own named types.

Also, note the use of a json.Decoder rather than reading in all the data ahead of time. Usually try and avoid any use of ioutil.ReadAll unless you really need all the data at once.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for your answer Dave! Actually using the Whatever structure help me solving the issue. Regarding ioutil.ReadAll I tried using r.Body directly but that did not work. Do you suggest to use io.Reader on r.Body?
@Dede the Body field of an http.Response is an io.Reader. Yes, use it via json.Decoder as I showed. Unless you need other fields/method of the request it'd also be better to change the argument of your getJsonContent function to be an io.Reader (like I did for fromJSON; or perhaps an io.ReadCloser; in the former you'd leave it to the caller to close the request body). That makes it more flexible (e.g. you could pass it an *os.File of stored JSON or a strings.Reader for testing).
Thanks, I actually managed to do exactly what you said with x, err := fromJSON(r.Body). By the way, just for clarity, RT is time.Duration and gtAnswer (ground truth answer) is string. Thanks again

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.