getsops/sops · error

Found non-string key %q when flattening

Error message

Found non-string key %q when flattening

What it means

flattenDescendMap walks a sops.TreeBranch and needs each TreeItem key to be a string so it can join path segments. Keys that are neither sops.Comment nor string (numbers, bools, nested structs) cannot be joined into the flattened path, so flattening aborts with this error.

Source

Thrown at stores/flatten.go:235

		Value: value,
	})
	(*destinationMap)[key] = true
	return destination, nil
}

func flattenDescendMap(branch sops.TreeBranch, prefix string, destination sops.TreeBranch, destinationMap *map[string]bool) (sops.TreeBranch, error) {
	for _, item := range branch {
		if _, ok := item.Key.(sops.Comment); ok {
			continue
		}
		if key, ok := item.Key.(string); ok {
			var err error
			destination, err = flattenDescendValue(item.Value, prefix+key, destination, destinationMap)
			if err != nil {
				return nil, err
			}
		} else {
			return nil, fmt.Errorf("Found non-string key %q when flattening", item.Key)
		}
	}
	return destination, nil
}

func flattenDescendArray(array []interface{}, prefix string, destination sops.TreeBranch, destinationMap *map[string]bool) (sops.TreeBranch, error) {
	i := 0
	for _, item := range array {
		if _, ok := item.(sops.Comment); ok {
			continue
		}
		var err error
		destination, err = flattenDescendValue(item, fmt.Sprintf("%s%d", prefix, i), destination, destinationMap)
		if err != nil {
			return nil, err
		}
		i++
	}

View on GitHub (pinned to 13442bb981)

Solutions

  1. Convert all non-string keys to strings (fmt.Sprintf("%v", key)) before flattening
  2. Represent arrays as []interface{} values under a string key instead of numeric keys in the branch
  3. Use sops.Comment{} for comment entries so they are skipped rather than rejected
  4. Parse the document with the store matching its format so keys are normalized

Example fix

// before
sops.TreeBranch{{Key: 0, Value: "first"}}

// after
sops.TreeBranch{{Key: "items", Value: []interface{}{"first"}}}
Defensive patterns

Strategy: type-guard

Validate before calling

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

Type guard

func allStringKeys(branch sops.TreeBranch) bool {
	for _, item := range branch {
		if _, isComment := item.Key.(sops.Comment); isComment {
			continue
		}
		if _, ok := item.Key.(string); !ok {
			return false
		}
	}
	return true
}

Try / catch

flat, err := flattenTreeBranch(branch, "")
if err != nil {
	if strings.Contains(err.Error(), "non-string key") {
		return fmt.Errorf("normalize keys before flattening: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: Calling flattenTreeBranch (directly or via stores.Flatten/MetadataFlattenBelowTop inside ExtractMetadata or ini EmitEncryptedFile's SerializeMetadata) on a branch where a nested item's Key is an int/bool/etc., e.g. sops.TreeItem{Key: 0, Value: ...} inside a subtree.

Common situations: Trees produced by other format stores that use numeric keys for arrays and were never converted; hand-assembled trees mixing types; passing a YAML/JSON-parsed branch straight into INI metadata serialization where array indices were left as ints.

Related errors


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