7

I'm retrieving JSON from a third party website (home electricity usage), and depending on what I've requested from the site, the JSON returned may or may not be an array. For example, if I request a list of my smart meters, I get this (results truncated, due to large size):

{"gwrcmds":{"gwrcmd":{"gcmd":"SPA_UserGetSmartMeterList","gdata":{"gip":{"version":"1"...

Where gwrcmd is a single element.

But if I request electricity usage for the last half hour, I get this:

{"gwrcmds":{"gwrcmd":[{"gcmd":"DeviceGetChart","gdata":{"gip":{"version":"1" ...

See how gwrcmd is now an array?

Within my Go app, I have a struct that looks like this (again, truncated, as it goes on for a while. There's more sub-structs and properties beneath "Version":

type Response struct {
    Gwrcmds struct {
        Gwrcmd struct {
            Gcmd  string
            Gdata struct {
                Gip struct {
                    Version string

If gwrcmd is an array, Gwrcmd needs to be a []struct { }, but if it's not, it's just a regular old struct { }

The problem is that json.Unmarshal just returns an error if the JSON has an array and the struct does not have a slice (or vice versa).

Would I need to create a second struct that duplicates the first one (except with a []struct { } instead), or is there a better way to do it? I thought of something with interfaces, but I haven't really touched those yet, so I'm not 100% sure on them.

djd
  • 4,988
  • 2
  • 25
  • 35
Grayda
  • 1,870
  • 2
  • 25
  • 43
  • One idea that pops to my head but seems sub-optimal is to implement an `io.Reader` which adds the `[` before a single `gcmd` structure and `]` at the end of it and then unmarshal from that reader. – thwd Sep 02 '15 at 07:58
  • In addition to the existing answers… you make request type A and get response type RA, you make request type B and get response type RB… perhaps you shouldn't try to read two different things (RA and RB) with the exact same code. It could be as simple as `type ResponseOne struct { Gwrcmdc struct { Gwrcmd GwCmdType } }` and `type ResponseMany struct { Gwrcmds struct { Gwrcmd []GwCmdType } }`. – Dave C Sep 02 '15 at 18:02

2 Answers2

10

Usually, whenever you have a JSON value of unknown type, you will use json.RawMessage to get it, peek into it, and unmarshal it correctly into the corresponding type. A simplified example:

// The A here can be either an object, or a JSON array of objects.
type Response struct {
    RawAWrapper struct {
        RawA json.RawMessage `json:"a"`
    }
    A  A   `json:"-"`
    As []A `json:"-"`
}

type A struct {
    B string
}

func (r *Response) UnmarshalJSON(b []byte) error {
    if err := json.Unmarshal(b, &r.RawAWrapper); err != nil {
        return err
    }
    if r.RawAWrapper.RawA[0] == '[' {
        return json.Unmarshal(r.RawAWrapper.RawA, &r.As)
    }
    return json.Unmarshal(r.RawAWrapper.RawA, &r.A)
}

Playground: http://play.golang.org/p/2d_OrGltDu.

Guessing the content based on the first byte doesn't seem too robust to me though. Usually you'll have some sort of a clue in your JSON (like a length or type field on the same level as the dynamic one) that tells you whether you have an object or an array.

See also:

Ainar-G
  • 34,563
  • 13
  • 93
  • 119
5

You can try to make custom json unmarshal method, like

func (a *GwrcmCustom) UnmarshalJSON(b []byte) (err error) {
    g, ga := Gwrcmd{}, []Gwrcmd{}
    if err = json.Unmarshal(b, &g); err == nil {
        *a = make([]Gwrcmd, 1)
        []Gwrcmd(*a)[0] = Gwrcmd(g)
        return
    }
    if err = json.Unmarshal(b, &ga); err == nil {
        *a = GwrcmCustom(ga)
        return
    }
    return
}

GwrcmCustom is a custom type, slice of Gwrcm

type GwrcmCustom []Gwrcmd

So we will get slice always

Try this on Go playground

I hope this will help

RoninDev
  • 5,446
  • 3
  • 23
  • 37