gohugoio/hugo · error

source must be a map, got %T

Error message

source must be a map, got %T

What it means

Thrown by Namespace.merge when a source parameter (any non-final param to Merge) is reflect-truthy but not reflect.Map. Sources contribute keys to the destination; a non-map source cannot be merged. The %T verb names the offending source type. Note a nil/zero source is silently skipped (IsTruthfulValue guard), so this fires only for truthy non-maps.

Solutions

  1. Pass only maps as sources: `{{ merge $overrides $defaults $dst }}`.
  2. Convert a struct to a map before merging (e.g. extract needed fields into a dict).
  3. Drop sources that are unexpectedly non-map.
  4. Validate each source is a map before the call.

Example fix

// before
{{ merge $pageList $dstMap }}
// after
{{ merge (dict "key" "val") $dstMap }}
Defensive patterns

Strategy: type-guard

Validate before calling

func mergeSourcesAreMaps(params ...any) error {
    for i := 0; i < len(params)-1; i++ {
        if !hreflect.IsTruthfulValue(reflect.ValueOf(params[i])) { continue }
        if reflect.ValueOf(params[i]).Kind() != reflect.Map {
            return fmt.Errorf("merge source must be map, got %T", params[i])
        }
    }
    return nil
}

Type guard

func isMap(v any) bool {
    if v == nil { return false }
    return reflect.ValueOf(v).Kind() == reflect.Map
}

Prevention

When it happens

Trigger: Calling `{{ merge $notAMap $dstMap }}` where $notAMap is a truthy slice, struct, or scalar. Because Merge iterates sources in reverse, each preceding param is validated here.

Common situations: Author passes a list or page object intending its fields to merge, or a partial receives heterogeneous args. A nil source is tolerated, but any truthy non-map triggers this.

Related errors


AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09). Data as JSON: /api/errors/b13a8f025ebea695. Report an issue: GitHub.

Appendix: source

Thrown at tpl/collections/merge.go:61

	}

	return result, nil
}

// merge creates a copy of dst and merges src into it.
func (ns *Namespace) merge(src, dst any) (any, error) {
	vdst, vsrc := reflect.ValueOf(dst), reflect.ValueOf(src)

	if vdst.Kind() != reflect.Map {
		return nil, fmt.Errorf("destination must be a map, got %T", dst)
	}

	if !hreflect.IsTruthfulValue(vsrc) {
		return dst, nil
	}

	if vsrc.Kind() != reflect.Map {
		return nil, fmt.Errorf("source must be a map, got %T", src)
	}

	if vsrc.Type().Key() != vdst.Type().Key() {
		return nil, fmt.Errorf("incompatible map types, got %T to %T", src, dst)
	}

	return mergeMap(vdst, vsrc).Interface(), nil
}

func caseInsensitiveLookup(m, k reflect.Value) (reflect.Value, bool) {
	if m.Type().Key().Kind() != reflect.String || k.Kind() != reflect.String {
		// Fall back to direct lookup.
		v := m.MapIndex(k)
		return v, hreflect.IsTruthfulValue(v)
	}

	k2 := reflect.New(m.Type().Key()).Elem()

View on GitHub (pinned to 52c9bd7908)