This question already has an answer here:
I'm consuming an endpoint (which I don't own and I cannot fix) and this endpoint returns JSON.
The problem is this JSON can come in different formats:
Format 1:
{
"message": "Message"
}
or
{
"message": ["ERROR_CODE"]
}
Depending on what happened.
I'd like to have one struct to hold this response so later I can check if the message
is a string or array, and properly follow a flow.
Is it possible to do it in Go? The first approach I thought was to have two structs and try to decode to the one with string
, and if an error happens, try to decode to the one with array
.
Is there a more elegant approach?
</div>
Unmarshal it into a value of interface{}
type, and use a type assertion or type switch to inspect the type of value it ends up. Note that by default JSON arrays are unmarshaled into a value of type []interface{}
, so you have to check for that to detect the error response.
For example:
type Response struct {
Message interface{} `json:"message"`
}
func main() {
inputs := []string{
`{"message":"Message"}`,
`{"message":["ERROR_CODE"]}`,
}
for _, input := range inputs {
var r Response
if err := json.Unmarshal([]byte(input), &r); err != nil {
panic(err)
}
switch x := r.Message.(type) {
case string:
fmt.Println("Success, message:", x)
case []interface{}:
fmt.Println("Error, code:", x)
default:
fmt.Println("Something else:", x)
}
}
}
Output (try it on the Go Playground):
Success, message: Message
Error, code: [ERROR_CODE]
You can use a custom type that implements json.Unmarshaller
and tries to decode each possible input format in turn:
type Message struct {
Text string
Codes []int
}
func (m *Message) UnmarshalJSON(input []byte) error {
var text string
err := json.Unmarshal(input, &text)
if err == nil {
m.Text = text
m.Codes = nil
return nil
}
var codes []int
err := json.Unmarshal(input, &codes)
if err == nil {
m.Text = nil
m.Codes = codes
return nil
}
return err
}
I prefer this approach over unmarshalling into interface{}
and type-asserting later because all the type checks are encapsulated in the unmarshalling step.
For a real-world example, check out my type veryFlexibleUint64
: https://github.com/sapcc/limes/blob/fb212143c5f5b3e9272994872fcc7b758ae47646/pkg/plugins/client_ironic.go
I would suggest creating a different type for Message and making that implement json.Unmarshaller
here is how the code would be
type message struct {
Text string
Codes []string //or int , assuming array of string as it was not mentioned in the question
}
func (m *message) UnmarshalJSON(input []byte) error {
if len(input) == 0 {
return nil
}
switch input[0] {
case '"':
m.Text = strings.Trim(string(input), `"`)
return nil
case '[':
return json.Unmarshal(input, &m.Codes)
default:
return fmt.Errorf(`invalid character %q looking for " or [`, input[0])
}
}
type Error struct {
Message message `json:"message"`
}
You may find full code with test here