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
- Inspect the source type with `{{ printf "%T" $src }}` and fix the data so it's a map.
- 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.
- Use `where`/`append` for slices; merge is only for dicts.
- 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
- Check every argument to merge is a map, not just the first
- Default missing params with `| default dict` so the source is always a map
- Remember merge takes maps only — use append/union for slices
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
- destination must be a map, got %T
- arguments to complement must be slices or arrays
- %s isn't a key of map type %s
- %s is neither a struct field, a method nor a map element of
- type %T not supported in Resource transformations
AI-assisted analysis of gohugoio/hugo@8a468df065 (2026-07-31).
Data as JSON: /data/errors/b13a8f025ebea695.json.
Report an issue: GitHub ↗.