Struct Tags & Custom Tag Parsing
Struct tags attach declarative metadata to fields.
Search across all documentation pages
Struct tags attach declarative metadata to fields.
Libraries read those strings at runtime with reflection or at build time with AST tools.
This page shows how to parse json, db, and validate tags and build a small custom binder.
Struct tags are backtick-quoted strings after a field declaration.
The reflect package exposes them as StructTag values with Get and Lookup.
Production parsers validate syntax, support multiple keys, and cache per-type field plans.
For hot paths, codegen reads the same tags from source and emits straight assignments.
Quick-reference tag parsing card.
// Field declaration
Name string `json:"name,omitempty" db:"user_name" validate:"required,min=2"`
// Read one key
tag := field.Tag.Get("json") // "name,omitempty"
name, opts, _ := strings.Cut(tag, ",")
// Lookup with ok flag
v, ok := field.Tag.Lookup("validate") // "required,min=2", trueWhen to reach for this:
go generate passA minimal env tag parser that fills a struct from a map.
package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
type Server struct {
Host string `env:"HOST" validate:"required"`
Port int `env:"PORT" validate:"min=1"`
}
type fieldPlan struct {
index int
key string
kind reflect.Kind
}
func plans(t reflect.Type) ([]fieldPlan, error) {
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("need struct")
}
var out []fieldPlan
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.IsExported() {
continue
}
key, ok := f.Tag.Lookup("env")
if !ok || key == "" {
continue
}
out = append(out, fieldPlan{index: i, key: key, kind: f.Type.Kind()})
}
return out, nil
}
func bind(ptr any, env map[string]string) error {
v := reflect.ValueOf(ptr)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return fmt.Errorf("ptr must be *struct")
}
v = v.Elem()
t := v.Type()
fp, err := plans(t)
if err != nil {
return err
}
for _, p := range fp {
raw, ok := env[p.key]
if !ok {
continue
}
fv := v.Field(p.index)
switch p.kind {
case reflect.String:
fv.SetString(raw)
case reflect.Int:
n, err := strconv.Atoi(raw)
if err != nil {
return fmt.Errorf("%s: %w", p.key, err)
}
fv.SetInt(int64(n))
default:
return fmt.Errorf("unsupported kind %s for %s", p.kind, p.key)
}
}
return nil
}
func main() {
s := &Server{}
err := bind(s, map[string]string{"HOST": "0.0.0.0", "PORT": "3000"})
fmt.Println(s, err)
}What this demonstrates:
Tag.Lookup distinguishes missing keys from empty valuesreflect.Type.Field(i) returns StructField with Name, Type, Tag, and offset.StructTag.Get(key) returns the value portion before the first comma for that key.`json:"x" xml:"x"`.| Key | Typical consumer | Value shape |
|---|---|---|
json | encoding/json | name,omitempty,string |
db | sqlx, GORM | column name, options |
validate | go-playground/validator | rules joined by commas |
yaml | gopkg.in/yaml.v3 | yaml name |
env | custom binders | environment variable name |
Split on commas, then parse key=value options:
func parseRules(tag string) map[string]string {
rules := map[string]string{}
for _, part := range strings.Split(tag, ",") {
k, v, ok := strings.Cut(strings.TrimSpace(part), "=")
if !ok {
rules[k] = "true"
continue
}
rules[k] = v
}
return rules
}// Cache plans per reflect.Type in a sync.Map for frameworks
var cache sync.Map // key: reflect.Type, value: []fieldPlanjson:"name" cause silent overwrites in encoding/json. Fix: Run go vet and custom linters in CI.fieldPlan slices go stale if you reload types in long-running plugins. Fix: Key cache by type identity and document hot-reload limits.Get returns "" for missing and empty alike. Fix: Use Lookup when presence matters.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Hand-written mapping | One or two DTOs | Dozens of similar structs |
go generate from tags | Hot path binding | Rapid prototype still changing shape |
| Struct embedding + methods | Behavior tied to types | Pure data transfer objects |
mapstructure decoder | Loose config maps | You need strict struct tags per field |
Backtick string with space-separated key:"value" pairs, e.g. `json:"id"`.
Values are always quoted strings.
It reflects on the type and reads the json key for field names and options like omitempty.
Yes.
Any key is valid; your parser or generator interprets it.
Some analyzers flag mismatched JSON tags or suspicious struct tags.
Enable govet and struct tag linters in CI.
db names SQL columns for ORMs and sqlx; json names wire fields.
The same struct often carries both.
Walk the type once, cache plans, reuse on each bind.
Per-request full reflection is fine only on cold paths.
Yes.
go/ast or go/types inspect field tags at codegen time.
stdlib JSON ignores unknown options; custom parsers should return errors at startup, not per request.
No.
They are implementation metadata, not documentation comments.
Table-test structs with different tag combinations and assert bound values and error messages.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026