Back to the posts

How to handle multiple JSON responses in Go

Introduction

If your API returns a single type JSON, you can use https://mholt.github.io/json-to-go/. But, it gets tricky if there are multiple structures in the responses. And, it gets even trickier if the embedded structures are different.

For simplicity, see the following example.

{
  "data": {
    "error_message": "not able to find data"
  }
}
{
  "data": {
    "message": "hello world"
  }
}

As you can see the root structure({ "data": { ... } }) is the same while the inner objects are different.

In Go, you can express in the following.

import "encoding/json"

// Root represents the following JSON
// {
//   "data": {
//      "message": "..."
//   }
// }
//
// or
//
// {
//   "data": {
//      "error_message": "..."
//   }
// }
type Root struct {
	Data DataWrapper `json:"data"`
}

type DataWrapper struct {
	ErrorData   *ErrorDataType   `json:"-"`
	MessageData *MessageDataType `json:"-"`
}

// UnmarshalJSON will receive either
//
// 1. ErrorDataType
// ```json
// {
//   "error_message": "error blah blah"
// }
// ```
//
// 2. MessageDataType
// ```json
// {
//   "message": "blah blah"
// }
// ```
func (d *DataWrapper) UnmarshalJSON(data []byte) error {
	var m map[string]interface{}
	var err error

	if err = json.Unmarshal(data, &m); err != nil {
		return err
	}

	if _, ok := m["error_message"]; ok {
		var errorData ErrorDataType
		err = json.Unmarshal(data, &errorData)
		if err != nil {
			return err
		}
		d.ErrorData = &errorData
		return nil
	}

	if _, ok := m["message"]; ok {
		var messageData MessageDataType
		err = json.Unmarshal(data, &messageData)
		if err != nil {
			return err
		}
		d.MessageData = &messageData
		return nil
	}

	return nil
}

type ErrorDataType struct {
	ErrorMessage string `json:"error_message"`
}

type MessageDataType struct {
	Message string `json:"message"`
}

The idea is that I created an wrapper object that only exists in Go.

type DataWrapper struct {
	ErrorData   *ErrorDataType   `json:"-"`
	MessageData *MessageDataType `json:"-"`
}

And then this struct implements UnmarshalJSON method that deals with the sub message portion of the JSON.

Once we are done, we can simply unmarshal without knowing the implementation details.

func TestRoot_UnmarshalErrorData(t *testing.T) {
	input := `
  {
    "data": {
      "error_message": "this is error_message"
    }
  }
  `

	var root Root

	err := json.Unmarshal([]byte(input), &root)
	assert.NoError(t, err)

	assert.Nil(t, root.Data.MessageData)
	assert.Equal(t, root.Data.ErrorData.ErrorMessage, "this is error_message")
}

func TestRoot_UnmarshalMessageData(t *testing.T) {
	input := `
  {
    "data": {
      "message": "this is message"
    }
  }
  `

	var root Root

	err := json.Unmarshal([]byte(input), &root)
	assert.NoError(t, err)

	assert.Nil(t, root.Data.ErrorData)
	assert.Equal(t, root.Data.MessageData.Message, "this is message")
}

© 2024 Mo Kweon. All rights reserved.