getsops/sops · error

Found non-string key %q when unflattening

Error message

Found non-string key %q when unflattening

What it means

unflattenTreeBranch converts a flattened TreeBranch (dotted paths like "a__map_b__list_0") back into a nested sops.TreeBranch. It expects every item's Key to be a Go string produced by Flatten. When a key is neither a sops.Comment nor a string (e.g. int, bool, sops.Comment is skipped, but other types pass through), this error aborts the round-trip because tokenize() cannot build a path from a non-string key.

Source

Thrown at stores/flatten.go:189

		result[k] = value
	}
	return result, nil
}

func unflattenTreeBranch(branch sops.TreeBranch) (sops.TreeBranch, error) {
	root := &node{}
	for _, item := range branch {
		if _, ok := item.Key.(sops.Comment); ok {
			continue
		}
		if key, ok := item.Key.(string); ok {
			tokens := tokenize(key)
			err := place(&item.Value, tokens, root)
			if err != nil {
				return nil, fmt.Errorf("Error while unflattening %q: %w", key, err)
			}
		} else {
			return nil, fmt.Errorf("Found non-string key %q when unflattening", item.Key)
		}
	}
	result, err := convert(root)
	if err != nil {
		return nil, fmt.Errorf("Error while unflattening: %w", err)
	}
	if tb, ok := result.(sops.TreeBranch); ok {
		return tb, nil
	}
	return nil, fmt.Errorf("Internal error: cannot find root")
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Flatten

func flattenDescendValue(value interface{}, key string, destination sops.TreeBranch, destinationMap *map[string]bool) (sops.TreeBranch, error) {
	switch value := value.(type) {
	case sops.TreeBranch:

View on GitHub (pinned to 13442bb981)

Solutions

  1. Convert every top-level key to a string before calling unflattenTreeBranch/ExtractMetadata (e.g. fmt.Sprintf("%v", key))
  2. Wrap numeric keys used for array positions in the expected flattened syntax (a__list_0) instead of using raw non-string keys
  3. Skip or drop non-string items from the branch, converting them to sops.Comment if they are comments
  4. Load the data through the store's LoadPlainFile/LoadEncryptedFile so keys are normalized to strings

Example fix

// before
branch := sops.TreeBranch{{Key: 1, Value: "x"}}
_, err := stores.ExtractMetadata(sops.TreeBranches{branch}, opts)

// after
branch := sops.TreeBranch{{Key: "1", Value: "x"}}
_, err := stores.ExtractMetadata(sops.TreeBranches{branch}, opts)
Defensive patterns

Strategy: validation

Validate before calling

for _, item := range branch {
	if _, ok := item.Key.(sops.Comment); ok {
		continue
	}
	if _, ok := item.Key.(string); !ok {
		return fmt.Errorf("top-level key %v is not a string", item.Key)
	}
}

Type guard

func isStringKey(key interface{}) bool {
	_, ok := key.(string)
	return ok
}

Try / catch

tb, err := unflattenTreeBranch(branch)
if err != nil {
	if strings.Contains(err.Error(), "non-string key") {
		// normalize keys to strings and retry
	}
	return err
}

Prevention

When it happens

Trigger: Calling stores.ExtractMetadata (with Flatten enabled) on TreeBranches whose sops branch contains top-level TreeItem keys that are not strings and not sops.Comment values — for example a TreeItem{Key: 42, Value: ...} or a key produced programmatically as an int.

Common situations: Hand-built TreeBranches passed to sops instead of data loaded from a real store; custom store implementations that emit numeric keys (like JSON/YAML do for array indices) fed into the INI-related metadata extraction path; programmatic tree construction where keys were not normalized to strings via fmt.Sprintf.

Related errors


AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01). Data as JSON: /api/errors/baedd7b154fedf11. Report an issue: GitHub.