Search code examples
jsongounmarshalling

Unmarshal JSON tagged union in Go


I'm trying to demarshal the JSON requests of Google Actions. These have arrays of tagged unions like this:

{
    "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
    "inputs": [{
      "intent": "action.devices.QUERY",
      "payload": {
        "devices": [{
          "id": "123",
          "customData": {
            "fooValue": 74,
            "barValue": true,
            "bazValue": "foo"
          }
        }, {
          "id": "456",
          "customData": {
            "fooValue": 12,
            "barValue": false,
            "bazValue": "bar"
          }
        }]
      }
    }]
}

{
    "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
    "inputs": [{
      "intent": "action.devices.EXECUTE",
      "payload": {
        "commands": [{
          "devices": [{
            "id": "123",
            "customData": {
              "fooValue": 74,
              "barValue": true,
              "bazValue": "sheepdip"
            }
          }, {
            "id": "456",
            "customData": {
              "fooValue": 36,
              "barValue": false,
              "bazValue": "moarsheep"
            }
          }],
          "execution": [{
            "command": "action.devices.commands.OnOff",
            "params": {
              "on": true
            }
          }]
        }]
      }
    }]
}

etc.

Obviously I can demarshal this to an interface{} and use fully dynamic type casts and everything to decode it, but Go has decent support for decoding to structs. Is there a way to do this elegantly in Go (like you can in Rust for example)?

I feel like you could almost do it by reading demarshalling to this initially:

type Request struct {
    RequestId string
    Inputs    []struct {
        Intent   string
        Payload  interface{}
    }
}

However once you have the Payload interface{} there doesn't seem to be any way to deserialise that into a struct (other than serialising it and deserialising it again which sucks. Is there any good solution?


Solution

  • Instead of unmarshaling Payload to an interface{} you can store it as a json.RawMessage and then unmarshal it based on the value of Intent. This is shown in the example in the json docs:

    https://golang.org/pkg/encoding/json/#example_RawMessage_unmarshal

    Using that example with your JSON and struct your code becomes something like this:

    type Request struct {
        RequestId string
        Inputs    []struct {
            Intent   string
            Payload  json.RawMessage
        }
    }
    
    var request Request
    err := json.Unmarshal(j, &request)
    if err != nil {
        log.Fatalln("error:", err)
    }
    for _, input := range request.Inputs {
        var payload interface{}
        switch input.Intent {
        case "action.devices.EXECUTE":
            payload = new(Execute)
        case "action.devices.QUERY":
            payload = new(Query)
        }
        err := json.Unmarshal(input.Payload, payload)
        if err != nil {
            log.Fatalln("error:", err)
        }
        // Do stuff with payload
    }