Skip to content

Field validation

Field validation is the optional net between “type-safe in Go” and “template may still reference a field that does not exist”. Enable it and templator verifies, when a template is first loaded, that the fields it references exist in the data type.

type ArticleData struct {
Title string
Content string
}
reg, err := templator.NewRegistry[ArticleData](
fsys,
templator.WithFieldValidation(ArticleData{}),
)
_, err = reg.Get("article")

You pass an instance of the data type (ArticleData{}). It is only used to bind the type parameter — the registry derives the type from T; the value’s contents are irrelevant. A zero value is conventional.

Validation happens once per template, at first Get, before the content is parsed. This order matters:

  • A field mismatch fails the Get call with a *templator.ValidationError — typically at startup or on the first render of the template.
  • Templates are cached, so the cost is paid once, not per render.
  • Templates that reference only valid fields behave exactly as without validation. There is no runtime overhead once a template is cached.

Validation extracts field references from the template source and walks each one against the reflected type of T (following nested structs and dereferencing pointers). Three shapes of reference are extracted:

// 1. Whole-action field references, with optional whitespace:
{{.Title}}
{{ .Title }}
// 2. Nested field paths, dotted:
{{.User.Name}}
// 3. Fields inside an if header:
{{if .User.Admin}}...{{end}}

For each reference, templator checks that the path resolves in the data type. In the example above, ArticleData must have a Title field, and User.Name must resolve through a struct (or pointer-to-struct) field User with a field Name. A missing field produces:

template 'article' validation error: 'Author' - field 'Author' not found in type ArticleData

*ValidationError carries the pieces for programmatic checks — the Err field holds the per-field cause, as detailed in Errors:

type ValidationError struct {
TemplateName string
FieldPath string
Err error
}

The data type itself must be a struct (or a pointer to one). A registry whose T is, say, a map or a slice is rejected at Get time with a validation error like expected struct type, got map.

What it does not check — and where it is wrong

Section titled “What it does not check — and where it is wrong”

The extractor matches the three patterns above and nothing else. These constructs are not checked at all:

  • {{range .Items}} and {{with .X}} (fields inside them are not extracted unless they also appear as one of the three covered shapes)
  • Pipelines such as {{.Title | upper}}
  • Variables ($x), computed expressions, and {{template "partial" .}} includes

Templates using only these constructs pass validation unhindered — silence is the worst case for them.

Three covered or adjacent shapes behave incorrectly today, confirmed against the implementation:

  1. Method calls inside covered shapes are rejected. {{.Footer}} where Footer is a method (not a field) matches the pattern, but the check looks at struct fields only, so Get fails with a validation error for a template that renders fine:

    template 't' validation error: 'Footer' - 'field 'Footer' not found in type Data'

    If your templates call methods, do not enable WithFieldValidation for those registries.

  2. Dotted paths through non-struct fields currently panic. A path like {{.Meta.Key}} where Meta is a map (or slice, or interface) passes the regex but the path walk calls FieldByName on a non-struct type, crashing Get without the error return:

    panic: reflect: FieldByName of non-struct type map[string]string

    Keep validated templates to plain nested structs and pointer chains.

  3. Unexported fields pass validation but fail to render. A template referencing {{.title}} (unexported) loads cleanly, then fails at Execute — a false negative within the covered shapes.

In short: for the safe subset — exported struct fields, nested structs, pointer chains — validation is dependable; outside that subset it is anything from silent to outright wrong. Drop WithFieldValidation for any template that calls methods or walks into maps, and keep regression tests exercising real renders (Test templates).