hashicorp/nomad · error

error parsing: root should be an object

Error message

error parsing: root should be an object

What it means

ParseConfigFile parses an agent config file twice: first via hcl.Decode into the config struct, then via hcl.Parse to hand-extract 'vault', 'consul', and 'keyring' blocks. This error is returned when the second hcl.Parse result's root AST node is not an *ast.ObjectList, meaning the file's top level is not an object/block structure HCL can filter.

Source

Thrown at command/agent/config_parse.go:85

		Telemetry: &Telemetry{},
		Vaults:    []*config.VaultConfig{},
		Reporting: config.DefaultReporting(),
	}

	err = hcl.Decode(c, buf.String())
	if err != nil {
		return nil, fmt.Errorf("failed to decode HCL file %s: %w", path, err)
	}

	// Re-parse the file to extract the multiple Vault configurations, which we
	// need to parse by hand because we don't have a label on the block
	root, err := hcl.Parse(buf.String())
	if err != nil {
		return nil, fmt.Errorf("failed to parse HCL file %s: %w", path, err)
	}
	list, ok := root.Node.(*ast.ObjectList)
	if !ok {
		return nil, fmt.Errorf("error parsing: root should be an object")
	}
	matches := list.Filter("vault")
	if len(matches.Items) > 0 {
		if err := parseVaults(c, matches); err != nil {
			return nil, fmt.Errorf("error parsing 'vault': %w", err)
		}
	}
	matches = list.Filter("consul")
	if len(matches.Items) > 0 {
		if err := parseConsuls(c, matches); err != nil {
			return nil, fmt.Errorf("error parsing 'consul': %w", err)
		}
	}

	matches = list.Filter("keyring")
	if len(matches.Items) > 0 {
		if err := parseKeyringConfigs(c, matches); err != nil {
			return nil, fmt.Errorf("error parsing 'keyring': %w", err)

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Open the config file and ensure the top level is an HCL object/block structure (e.g. `vault { ... }` or key = value at root), not a bare value or array
  2. If the file is JSON, wrap contents in a top-level object `{ ... }` instead of an array or scalar
  3. Verify the path passed to LoadConfig points to a real HCL config file, not a directory or binary
  4. Re-save the file as UTF-8 without a BOM, which can confuse the HCL parser

Example fix

// before (config.hcl)
["vault", "consul"]

// after
vault {}
consul {}
Defensive patterns

Strategy: validation

Validate before calling

func validateHCLRoot(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	trimmed := bytes.TrimSpace(data)
	if len(trimmed) == 0 {
		return fmt.Errorf("%s is empty", path)
	}
	if trimmed[0] == '[' || trimmed[0] == '"' || trimmed[0] == '-' {
		return fmt.Errorf("%s: top-level value is not an HCL object", path)
	}
	return nil
}

Type guard

func isHCLObjectNode(n ast.Node) bool {
	_, ok := n.(*ast.ObjectList)
	return ok
}

Try / catch

cfg, err := ParseConfigFile(path)
if err != nil {
	if strings.Contains(err.Error(), "root should be an object") {
		return fmt.Errorf("config %s has no top-level object; check file contents", path)
	}
	return err
}

Prevention

When it happens

Trigger: Calling ParseConfigFile (directly or via LoadConfig/LoadConfigDir) on an HCL file whose top-level node parses to something other than an ObjectList (e.g. a bare scalar or list at the top level, or a malformed/empty file producing an unexpected AST root).

Common situations: Config file containing only a JSON array, a bare value like "abc", or a file corrupted/truncated so hcl.Parse returns a non-object root; also seen when a non-HCL file is passed as a config path.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/9c8a93357558c39a. Report an issue: GitHub.