As of October 1, 2023, LINE has been rebranded as LY Corporation. Visit the new blog of LY Corporation here: LY Corporation Tech Blog

Blog


Making a Flex Message with LINE's Go SDK

Hi there, this is @sugyan (TwitterGitHub) 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 headerbody 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 BoxComponents and ButtonComponents 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!)