Marshal and Unmarshal HCL Data (1/2)

Peter Bi
4 min readMay 27, 2023

--

Marshal GO Object into HCL

1. Introduction

According to Hashicorp, HCL (Hashicorp Configuration Language) is a toolkit for creating structured configuration languages that are both human- and machine-friendly, for use with command-line tools. Whereas JSON and YAML are formats for serializing data structures, HCL is a syntax and API specifically designed for building structured configuration formats.

HCL is a key component of Hashicorp’s cloud infrastructure automation tools, such as Terraform. Its robust support for configuration and expression syntax gives it the potential to serve as a server-side format. For instance, it could replace the backend programming language in low-code/no-code platforms. However, the current HCL library does not fully support some data types, such as map and interface, which limits its usage.

To address this, we have developed a new Go package, determined, which implements marshal and unmarshal functions for HCL, akin to those for JSON.

In this article, we will explore how to encode objects into HCL data.
The decoding of HCL, including data with dynamic schemas, will be discussed in a subsequent article.

2. Encoding Map

Here is an example to encode object with package gohcl.

package main

import (
"fmt"

"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclwrite"
)

type square struct {
SX int `json:"sx" hcl:"sx"`
SY int `json:"sy" hcl:"sy"`
}

func (self *square) Area() float32 {
return float32(self.SX * self.SY)
}

type geometry struct {
Name string `json:"name" hcl:"name"`
Shapes map[string]*square `json:"shapes" hcl:"shapes"`
}

func main() {
app := &geometry{
Name: "Medium Article",
Shapes: map[string]*square{
"k1": &square{SX: 2, SY: 3}, "k2": &square{SX: 5, SY: 6}},
}

f := hclwrite.NewEmptyFile()
gohcl.EncodeIntoBody(app, f.Body())
fmt.Printf("%s", f.Bytes())
}

It panics because of the map field Shapes.

panic: cannot encode map[string]*main.square as HCL expression: no cty.Type for main.square (no cty field tags)

But determined will encode it properly:

package main

import (
"fmt"

"github.com/genelet/determined/dethcl"
)

type square struct {
SX int `json:"sx" hcl:"sx"`
SY int `json:"sy" hcl:"sy"`
}

func (self *square) Area() float32 {
return float32(self.SX * self.SY)
}

type geometry struct {
Name string `json:"name" hcl:"name"`
Shapes map[string]*square `json:"shapes" hcl:"shapes"`
}

func main() {
app := &geometry{
Name: "Medium Article",
Shapes: map[string]*square{
"k1": &square{SX: 2, SY: 3}, "k2": &square{SX: 5, SY: 6}},
}

bs, err := dethcl.Marshal(app)
if err != nil {
panic(err)
}
fmt.Printf("%s", bs)
}

Run the code:

# run the code, we get:
$ go run sample1_2.go

name = "Medium Article"
shapes k1 {
sx = 2
sy = 3
}

shapes k2 {
sx = 5
sy = 6
}

Note:

map is encoded as block list with labels as keys.

3. Encode Interface Data

Go struct picture has field Drawings, a list of interface. This sample shows how determined encodes data of one square and one circle in the list.

package main

import (
"fmt"
"github.com/genelet/determined/dethcl"
)

type inter interface {
Area() float32
}

type square struct {
SX int `json:"sx" hcl:"sx"`
SY int `json:"sy" hcl:"sy"`
}

func (self *square) Area() float32 {
return float32(self.SX * self.SY)
}

type circle struct {
Radius float32 `json:"radius" hcl:"radius"`
}

func (self *circle) Area() float32 {
return 3.14159 * self.Radius
}

type picture struct {
Name string `json:"name" hcl:"name"`
Drawings []inter `json:"drawings" hcl:"drawings"`
}

func main() {
app := &picture{
Name: "Medium Article",
Drawings: []inter{
&square{SX: 2, SY: 3}, &circle{Radius: 5.6}},
}

bs, err := dethcl.Marshal(app)
if err != nil {
panic(err)
}
fmt.Printf("%s", bs)
}

Run the code:

$ go run sample1_3.go 

name = "Medium Article"
drawings {
sx = 2
sy = 3
}

drawings {
radius = 5.599999904632568
}

4. Encoding with HCL Labels

label is encoded as map key. If it is missing, the block map will be encoded as list:

package main

import (
"fmt"
"github.com/genelet/determined/dethcl"
)

type inter interface {
Area() float32
}

type square struct {
SX int `json:"sx" hcl:"sx"`
SY int `json:"sy" hcl:"sy"`
}

func (self *square) Area() float32 {
return float32(self.SX * self.SY)
}

type moresquare struct {
Morename1 string `json:"morename1", hcl:"morename1,label"`
Morename2 string `json:"morename2", hcl:"morename2,label"`
SX int `json:"sx" hcl:"sx"`
SY int `json:"sy" hcl:"sy"`
}

func (self *moresquare) Area() float32 {
return float32(self.SX * self.SY)
}

type picture struct {
Name string `json:"name" hcl:"name"`
Drawings []inter `json:"drawings" hcl:"drawings"`
}

func main() {
app := &picture{
Name: "Medium Article",
Drawings: []inter{
&square{SX: 2, SY: 3},
&moresquare{Morename1: "abc2", Morename2: "def2", SX: 2, SY: 3},
},
}

bs, err := dethcl.Marshal(app)
if err != nil {
panic(err)
}
fmt.Printf("%s", bs)
}

Run the code:

$ go run sample1_5.go 
name = "Medium Article"
drawings {
sx = 2
sy = 3
}

drawings "abc2" "def2" {
sx = 2
sy = 3
}

The labels abc2 and def2 are properly placed in block Drawings.

5. Summary

The new HCL package, determined, can marshal a wider range of Go objects, such as interfaces and maps, bringing HCL a step closer to becoming a universal data interchange format like JSON and YAML.

--

--