gohugoio/hugo · error

source must be a map, got %T

Error message

source must be a map, got %T

What it means

After confirming the destination is a map, Hugo's `merge` checks each source parameter. A source that is truthy (non-nil, non-empty) but not a map cannot be merged, so merge fails with this error naming the offending type. Falsy sources (nil, empty) are silently skipped and the destination is returned unchanged.

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 8a468df065)

Solutions

  1. Inspect the source type with `{{ printf "%T" $src }}` and fix the data so it's a map.
  2. If the source may legitimately be empty/missing, that's fine (it's skipped) — but ensure non-empty values are maps in all content files/data files.
  3. Use `where`/`append` for slices; merge is only for dicts.
  4. Normalize params across pages via cascade in front matter so the key always has map shape.

Example fix

# before (front matter)
extra:
  - one
  - two
# after
extra:
  first: one
  second: two
Defensive patterns

Strategy: type-guard

Validate before calling

{{ if and (reflect.IsMap $dst) (reflect.IsMap $src) }}{{ $merged := merge $dst $src }}{{ end }}

Type guard

{{ $bothMaps := and (reflect.IsMap $dst) (reflect.IsMap $src) }}

Try / catch

{{ with try (merge $dst $src) }}{{ with .Err }}{{ warnf "merge source invalid: %s" . }}{{ end }}{{ end }}

Prevention

When it happens

Trigger: `{{ merge $src $dst }}` where `$src` is a non-empty slice, string, number, or other non-map value. With more than two parameters, any earlier parameter that is a non-empty non-map triggers it, e.g. `merge .Params.list site.Params.opts`.

Common situations: Front matter key expected to be a map is authored as a list; passing `.Pages` or the result of `where` into merge; merging `.Params.foo` on pages where some pages define `foo` as a string; upgrading themes where a params key changed shape.

Related errors


AI-assisted analysis of gohugoio/hugo@8a468df065 (2026-07-31). Data as JSON: /data/errors/b13a8f025ebea695.json. Report an issue: GitHub ↗.