Decoding With Dynamic Schema with the hcldec package

In section Decoding Into Native Go Values with the gohcl package, we saw the most straightforward way to access the content from an HCL file, decoding directly into a Go value whose type is known at application compile time.

For some applications, it is not possible to know the schema of the entire configuration when the application is built. For example, HashiCorp Terraform uses HCL as the foundation of its configuration language, but parts of the configuration are handled by plugins loaded dynamically at runtime, and so the schemas for these portions cannot be encoded directly in the Terraform source code.

HCL’s hcldec package offers a different approach to decoding that allows schemas to be created at runtime, and the result to be decoded into dynamically-typed data structures.

The sections below are an overview of the main parts of package hcldec. For full details, see the package godoc.

Decoder Specification

Whereas gohcl infers the expected schema by using reflection against the given value, hcldec obtains schema through a decoding specification, which is a set of instructions for mapping HCL constructs onto a dynamic data structure.

The hcldec package contains a number of different specifications, each implementing hcldec.Spec and having a Spec suffix on its name. Each spec has two distinct functions:

  • Adding zero or more validation constraints on the input configuration file.

  • Producing a result value based on some elements from the input file.

The most common pattern is for the top-level spec to be a hcldec.ObjectSpec with nested specifications defining either blocks or attributes, depending on whether the configuration file will be block-structured or flat.

spec := hcldec.ObjectSpec{
    "io_mode": &hcldec.AttrSpec{
        Name: "io_mode",
        Type: cty.String,
    },
    "services": &hcldec.BlockMapSpec{
        TypeName:   "service",
        LabelNames: []string{"type", "name"},
        Nested:     hcldec.ObjectSpec{
            "listen_addr": &hcldec.AttrSpec{
                Name:     "listen_addr",
                Type:     cty.String,
                Required: true,
            },
            "processes": &hcldec.BlockMapSpec{
                TypeName:   "service",
                LabelNames: []string{"name"},
                Nested:     hcldec.ObjectSpec{
                    "command": &hcldec.AttrSpec{
                        Name:     "command",
                        Type:     cty.List(cty.String),
                        Required: true,
                    },
                },
            },
        },
    },
}
val, moreDiags := hcldec.Decode(f.Body, spec, nil)
diags = append(diags, moreDiags...)

The above specification expects a configuration shaped like our example in Introduction to HCL, and calls for it to be decoded into a dynamic data structure that would have the following shape if serialized to JSON:

{
  "io_mode": "async",
  "services": {
    "http": {
      "web_proxy": {
        "listen_addr": "127.0.0.1:8080",
        "processes": {
          "main": {
            "command": ["/usr/local/bin/awesome-app", "server"]
          },
          "mgmt": {
            "command": ["/usr/local/bin/awesome-app", "mgmt"]
          }
        }
      }
    }
  }
}