Hi there, this is @sugyan (Twitter, GitHub) from LINE Kyoto office. This was first time for me to experience Kyoto's Summer, and boy, was it really hot or what!? (The original post in Japanese was published in August 2018.)
In June 2018, a new message type, Flex Message, was introduced in the LINE Message API, allowing more complex message layouts to customize messages. To use it, see Using Flex Messages and Messaging API reference for details. The official SDK supports Flex Message too. As a person in charge of the Go SDK, I'd like to share some of the lessons I've learned from adding the support for Flex Message.
Using Flex Message with LINE's Go SDK
What the following code does is returning a Flex Message to a received message. The ReplyMessage()
function of linebot.Client delivers an object implementing the Message
interface. Likewise, the FlexMessage
object created with the NewFlexMessage()
function is an implementation of the Message
interface.
The second parameter of the NewFlexMessage()
function is an implementation of the FlexContainer
interface. Since BubbleContainer
or CarouselContainer
implements the interface, you can use either one. Our sample code is using the BubbleContainer
.
import "github.com/line/line-bot-sdk-go/linebot"
func reply(replyToken string) error {
container := &linebot.BubbleContainer{
Type: linebot.FlexContainerTypeBubble,
Body: &linebot.BoxComponent{
Type: linebot.FlexComponentTypeBox,
Layout: linebot.FlexBoxLayoutTypeHorizontal,
Contents: []linebot.FlexComponent{
&linebot.TextComponent{
Type: linebot.FlexComponentTypeText,
Text: "Hello,",
},
&linebot.TextComponent{
Type: linebot.FlexComponentTypeText,
Text: "World!",
},
},
},
}
if _, err := client.ReplyMessage(
replyToken,
linebot.NewFlexMessage("alt text", container),
).Do(); err != nil {
return err
}
return nil
}
Having to write a struct
representing each component or container can be bothersome, so I thought of using the Builder pattern, but the only benefit we could get from using Builder was not having to define the type
property; opting for Builder would require us to modify the type definitions, as well as the builder for specification changes. So, instead, we came up with a more practical way — writing a function that creates a FlexMessage
object out of a JSON string. Read on to find more about this function.
Using JSON generated by simulator
To create a Flex Message, you need to put JSON data into the request and POST the request. To relieve you from the burden of checking whether you've written good JSON data, use the simulator. The following screenshot shows the result of creating a sample message with a template, "Restaurant". The graphic image on the left-hand side is the visualization of the JSON data on the right. Changing any of the JSON data and pressing the Preview button will render the changes in the left panel right away.
If the JSON data specified on the simulator is rendered properly, the JSON data is valid to be POSTed as it is. Then, wouldn't it be better to have the SDK send the JSON data on the simulator? Well, for this to happen, we need to accept a byte array([]byte
) or string
as a parameter. There is a potential problem with this; invalid JSON data could be posted.
So what we did with our SDK is make a function, UnmarshalFlexMessageJSON([]byte)
that converts JSON data into an object that implements the FlexContainer
interface. This prohibits sending invalid JSON data, and at the same time, enables us to create a Flex Message.
jsonData := []byte(`{
"type": "bubble",
...
}`)
container, err: = linebot.UnmarshalFlexMessageJSON(jsonData)
// err is returned if invalid JSON is given that cannot be unmarshalled
if err != nil {
...
}
message: = linebot.NewFlexMessage("alt text", container)
Although you have to take that extra step of converting given JSON string into a Flex Message, you can use the JSON created by the simulator to send a Flex Message.
Unmarshaling JSON
Let's have a look at the implementation of the UnmarshalFlexMessageJSON([]byte)
function. The basic process consists of unmarshalJSON data using json.Unmarshal
first and then map each JSON property to the right type. This is not easy as it sounds, because we have no types defined to map each property.
Going back to what we covered previously, the FlexContainer
interface — the second parameter of the NewFlexMessage()
function — is implemented either with a BubbleContainer
or CarouselContainer
. These containers contain blocks such as header
, body
and footer
, each containing a contents
element which is an array of components that fill a given block.
With Flex Messages, this array would contain components such as BoxComponent
s and ButtonComponent
s that implements the FlexComponent
interface. The FlexComponent
interface may contain an action
element. Action elements are used by various objects, including Template Messages. An action element can be mapped to an implementation of the TemplateAction
interface.
Each JSON representation of containers, components, and actions, there is a type
property specifying the type it is representing. Which means you can obtain the type to unmarshal the data to, with the type
property. For example, if you see type
properties of the two objects in the contents
element, we can see that the first object shall be unmarshaled to an IconComponent
and the second to a TextComponent
.
{
...
"contents": [
{
"type": "icon",
"size": "sm",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png"
},
{
"type": "text",
"text": "4.0",
"size": "sm",
"color": "#999999",
"margin": "md",
"flex": 0
}
],
...
}
A type dedicated for unmarshaling
If we don't know the type to unmarshal JSON data to, we cannot call the json.Unmarshal
function. Before retrieving the value of the type
property, we have no way to identify the type of component; we cannot have the type defined before calling the json.Unmarshal
function. So what we did in our SDK is, prepare a type just for unmarshaling, find out the type of the component, and then unmarshal. Let's have a look at converting a FlexContainer object.
The rawFlexContainer
type is the point here. This is the type we use only for unmarshaling. Since we have the UnmarshalJSON()
function implemented, if rawFlexContainer
is passed to the json.Unmarshal()
function, then the UnmarshalJSON()
function gets called.
func UnmarshalFlexMessageJSON(data []byte) (FlexContainer, error) {
raw := rawFlexContainer{}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
return raw.Container, nil
}
type rawFlexContainer struct {
Type FlexContainerType json:"type"
Container FlexContainer json:"-"
}
func (c *rawFlexContainer) UnmarshalJSON(data []byte) error {
type alias rawFlexContainer
raw := alias{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
var container FlexContainer
switch raw.Type {
case FlexContainerTypeBubble:
container = &BubbleContainer{}
case FlexContainerTypeCarousel:
container = &CarouselContainer{}
default:
return errors.New("invalid container type")
}
if err := json.Unmarshal(data, container); err != nil {
return err
}
c.Type = raw.Type
c.Container = container
return nil
}
Using type aliases
Look at line 7 of the code below. The UnmarshalJSON()
function declares type alias rawFlexComponent
which contains the same content as the rawFlexContainer
, but has a different name. Because rawFlexComponent
is a type alias, the (*rawFlexContainer) UnmarshalJSON()
function is not called as in our previous example, when the type alias is passed to the json.Unmarshal()
function. The json
tags indicate that only the type
information is unmarhsaled out of the alias
type, but this is the information we need for mapping. We prepare an object corresponding for a given type, and then really unmarshal the data to the right type, using the json.Unmarshal()
function. In the end, we unmarshal given data twice, but we can identify the right type to map the data to.
Likewise, when we unmarshal the implementation of the FlexComponent
interface, we prepare the type rawFlexComponent
and then use a type alias to prepare the object for mapping.
type rawFlexComponent struct {
Type FlexComponentType json:"type"
Component FlexComponent json:"-"
}
func (c *rawFlexComponent) UnmarshalJSON(data []byte) error {
type alias rawFlexComponent
raw := alias{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
var component FlexComponent
switch raw.Type {
case FlexComponentTypeBox:
component = &BoxComponent{}
case FlexComponentTypeButton:
component = &ButtonComponent{}
case FlexComponentTypeFiller:
component = &FillerComponent{}
case FlexComponentTypeIcon:
component = &IconComponent{}
case FlexComponentTypeImage:
component = &ImageComponent{}
case FlexComponentTypeSeparator:
component = &SeparatorComponent{}
case FlexComponentTypeSpacer:
component = &SpacerComponent{}
case FlexComponentTypeText:
component = &TextComponent{}
default:
return errors.New("invalid flex component type")
}
if err := json.Unmarshal(data, component); err != nil {
return err
}
c.Type = raw.Type
c.Component = component
return nil
}
The following code unmarshals a BoxComponent
that contains Contents []FlexComponent
. If you look at line 8, an alias is using a struct that only overwrites Contents
; we can unmarshal the original BoxComponent
excluding the Contents
value. Because we unmarshal the Contents
element into a slice of rawFlexComponent,
we can take out the Component
part of each rawFlexComponent
unit and eventually we can obtain an unmarshaled outcome.
type rawFlexComponent struct {
Type FlexComponentType json:"type"
Component FlexComponent json:"-"
}
func (c *BoxComponent) UnmarshalJSON(data []byte) error {
type alias BoxComponent
raw := struct {
Contents []rawFlexComponent json:"contents"
*alias
}{
alias: (*alias)(c),
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
components := make([]FlexComponent, len(raw.Contents))
for i, content := range raw.Contents {
components[i] = content.Component
}
c.Contents = components
return nil
}
Replacing switch with reflect
I am not content with the part for sorting which object to map a particular data type using the switch
statement. By using reflect
instead, you can have object types prepared in advance, and dynamically create an object for a given type. For instance, for rawFlexContainer
, we can prepare the type name and reflect.Type
and rewrite the code like this.
import "reflect"
var containerTypeMap = map[FlexContainerType]reflect.Type{
FlexContainerTypeBubble: reflect.TypeOf(BubbleContainer{}),
FlexContainerTypeCarousel: reflect.TypeOf(CarouselContainer{}),
}
func (c *rawFlexContainer) UnmarshalJSON(data []byte) error {
type alias rawFlexContainer
raw := alias{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
container := reflect.New(containerTypeMap[raw.Type]).Interface()
if err := json.Unmarshal(data, container); err != nil {
return err
}
if flexContainer, ok := container.(FlexContainer); ok {
c.Container = flexContainer
}
return nil
}
Without the switch
statement it looks much neat, but for our SDK we didn't opt this on purpose. reflect
is convenient, but the consequences of misusing can be disastrous, making maintenance difficult as well. If using switch
can fulfill implementing your logic, there is no need to use reflect
. I think it's safe not to use it unless necessary; better be safe than sorry.
Lastly
So, I've shared how to use Flex Messages with our Go SDK. Handling the variety JSON data brings with a typed language Go was difficult, but it wasn't impossible. I'd like to express my special thank @mokejp!
(The posting was written in August, 2018. If you know better ways, please send pull requests and share them with me!)