7
\$\begingroup\$

I have JSON object response returned from the server which is similar to this one:

{
    "id": 123,
    "from_id": 234,
    "attachments": [
        {
            "type": "audio",
            "audio": {
                "artist": "Van Halen",
                "title": "Eruption"
            }
        },
        {
            "type": "photo",
            "photo": {
                "id": 123,
                "url": "http://someurl.com"
            }
        }
    ]
}

and I want to unmarshal it to the following structure:

type Response struct {
    Id          int `json:"id"`
    FromId      int `json:"from_id"`
    Attachments []Attachment
}

where Attachment is described like this

type Attachment struct {
    Type   string
    Fields map[string]interface{}
}

and Fields field is map that contains fields from "photo" or "audio" object of attachment in JSON.

Here is the code I've written to accomplish my task:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "reflect"
    "strings"
    "unicode"
)

type Response struct {
    Id          int `json:"id"`
    FromId      int `json:"from_id"`
    Attachments []Attachment
}

type Attachment struct {
    Type   string
    Fields map[string]interface{}
}

func (this *Attachment) UnmarshalJSON(b []byte) (err error) {
    type innerAttachment struct {
        Type  string
        Audio map[string]interface{}
        Photo map[string]interface{}
    }

    inAt := innerAttachment{}
    *this = Attachment{}

    if err = json.Unmarshal(b, &inAt); err == nil {
        this.Type = inAt.Type

        a := []rune(inAt.Type)
        a[0] = unicode.ToUpper(a[0])

        v := reflect.ValueOf(inAt)

        m := v.FieldByName(string(a)).Interface()
        this.Fields = m.(map[string]interface{})
    }

    return
}

func main() {
    encoded := `
    {
        "id": 123,
        "from_id": 234,
        "attachments": [
            {
                "type": "audio",
                "audio": {
                    "artist": "Van Halen",
                    "title": "Eruption"
                }
            },
            {
                "type": "photo",
                "photo": {
                    "id": 123,
                    "url": "http://someurl.com"
                }
            }
        ]
    }
    `

    response := &Response{}
    reader := strings.NewReader(encoded)
    err := json.NewDecoder(reader).Decode(&response)
    if err != nil {
        log.Fatal(err)
    }

    for _, v := range response.Attachments {
        fmt.Println(v.Type)
        for key, value := range v.Fields {
            fmt.Printf("%s => %s\n", key, value)
        }
        fmt.Println("")
    }
}

Is my code OK or maybe there's more Go'ish way to unmarshal JSON provided above?

\$\endgroup\$
2
  • \$\begingroup\$ Are all fields in JSON object required? \$\endgroup\$ Commented Nov 8, 2014 at 20:48
  • \$\begingroup\$ @GrzegorzŻur yes, all fields are required. \$\endgroup\$
    – rpeshkov
    Commented Nov 9, 2014 at 21:46

1 Answer 1

6
\$\begingroup\$

I would not implement method UnmarshalJSON as it is not really necessary. Instead, I would create Audio and Photo types and put them as fields into Attachment type. It would be easier to reference data as field than get it as a value from map.

package main

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

type Response struct {
    Id          int `json:"id"`
    FromId      int `json:"from_id"`
    Attachments []Attachment
}

type Audio struct {
    Artist string `json:"artist"`
    Title  string `json:"title"`
}

type Photo struct {
    Id  int    `json:"id"`
    Url string `json:"url"`
}

type Attachment struct {
    Type  string
    Audio `json:"audio"`
    Photo `json:"photo"`
}

func main() {
    encoded := `
    {
        "id": 123,
        "from_id": 234,
        "attachments": [
            {
                "type": "audio",
                "audio": {
                    "artist": "Van Halen",
                    "title": "Eruption"
                }
            },
            {
                "type": "photo",
                "photo": {
                    "id": 123,
                    "url": "http://someurl.com"
                }
            }
        ]
    }
    `

    response := &Response{}
    reader := strings.NewReader(encoded)
    err := json.NewDecoder(reader).Decode(&response)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(response)
}
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the code! Didn't know that embedding structs can be used that way. Unfortunately there are some caveats with embedding. Sorry that I didn't mentioned this in my question, but attachments can be not only audios and photos, but also other types. Some of them contain fields that is present in other attachment types and that will lead to ambiguous selector when I will try to access Id for example. \$\endgroup\$
    – rpeshkov
    Commented Nov 10, 2014 at 20:21
  • \$\begingroup\$ I know that I can resolve this using full path, i.e. Photo.Id but what if I want just iterate through all attachments and grab fields, present in any type? I will have to perform type assertion depending on actual type. Maybe there's some other way that I don't know? :) \$\endgroup\$
    – rpeshkov
    Commented Nov 10, 2014 at 20:22
  • \$\begingroup\$ @black_wizard If you want to iterate through all properties you could use visitor pattern. gist.github.com/francoishill/f0624e7760aacdc96b42 It would not be as concise as using reflection but in my opinion reflection should be the last resort. \$\endgroup\$ Commented Nov 11, 2014 at 7:18

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.