JimBlizz
JimBlizz

Reputation: 384

Unmarshal JSON that could be a string or an object

I have a third party service that is returning JSON where one field contains a collection of data. Here is an example of the structure it returns.

{
  "title": "Afghanistan",
  "slug": "afghanistan",
  "fields": {
    "fieldOne": "",
    "fieldTwo": {
      "new1": {
        "type": "contentBlock",,
        "fields": {
          "richTextBlick": "<p>This is the travel advice.<\/p>"
        }
      }
    },
    "fieldThree": {
      "new1": {
        "type": "introBlock",
        "fields": {
          "text": "This is a title"
          "richText": "<p>This is the current travel summary for Afganistan.<\/p>"
        }
      },
      "new2": {
        "type": "contentBlock",
        "fields": {
          "richText": "<p>It has a second block of content!<\/p>"
        }
      }
    },
    "fieldfour": "country"
  }
}

Each of the "field" entries could be either a string or another object. I'd like to decode these into something like the structs below.

type EntryVersion struct {
    Slug string `json:"slug"`
    Fields map[string][]EntryVersionBlock `json:"fields"`
}

type EntryVersionBlock struct {
    Type string `json:"type"`
    Fields map[string]string `json:"fields"`
}

If the field value is simply a string, I would wrap it in an EntryVersionBlock with the type of "Text" and a single entry in the Fields map.

Any ideas how can I do this in an efficient manner? I may have to do it several hundred times in an extreme edge-case.

Thanks

Upvotes: 0

Views: 1043

Answers (2)

mkopriva
mkopriva

Reputation: 38223

Your structure is a little bit off from the json's. Fields in EntryVersion is an object not an array so making it a slice is not what you want. I would recommend you change it to this:

type EntryVersion struct {
    Slug   string                       `json:"slug"`
    Fields map[string]EntryVersionBlock `json:"fields"`
}

The EntryVersionBlock does not have a "type" field in the corresponding json, it has entries with field names "new1", "new2", etc. So I would recommend you add a 3rd type Entry into which you unmarshal those fields.

type Entry struct {
    Type   string            `json:"type"`
    Fields map[string]string `json:"fields"`
}

And you would update your EntryVersionBlock to look something like this:

type EntryVersionBlock struct {
    Value  string           `json:"-"`
    Fields map[string]Entry `json:"fields"`
}

And to deal with your original problem you can have the EntryVersionBlock type implement the json.Unmarshaler interface, check the first byte in the data passed to the UnmarshalJSON method and if it's a double quote it's a string and if it's a curly opening brace it's an object. Something like this:

func (evb *EntryVersionBlock) UnmarshalJSON(data []byte) error {
    switch data[0] {
    case '"':
        if err := json.Unmarshal(data, &evb.Value); err != nil {
            return err
        }
    case '{':
        evb.Fields = make(map[string]Entry)
        if err := json.Unmarshal(data, &evb.Fields); err != nil {
            return err
        }
    }
    return nil
}

Playground: https://play.golang.org/p/IsTXI5202m

Upvotes: 4

Radi
Radi

Reputation: 6584

You can use GJson library to unmarshal your JSON to map, then iterate on this map and do the conversion that you need using your structs and based on fields type (map or string).

Update: Example using this method with similar case http://blog.serverbooter.com/post/parsing-nested-json-in-go/

Upvotes: 0

Related Questions