Skip to main content

Struct Tags and Metadata

Created: May 8, 2026 Larry Qu 7 min read

Struct tags provide a way to attach metadata to struct fields. They’re essential for serialization, validation, and database mapping. Go packages like encoding/json, encoding/xml, and ORM libraries all rely on struct tags to map fields between Go and external representations. For a broader view of how Go handles data transformation, see Encoding and Decoding Data.

Struct Tag Basics

Tags are string literals attached to struct fields following the field type declaration. Each tag is a raw string (key:"value") that packages parse using reflect — the same mechanism used for Reflection and Type Inspection.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Person struct {
	Name  string `json:"name"`
	Age   int    `json:"age"`
	Email string `json:"email,omitempty"`
}

func main() {
	p := Person{Name: "Alice", Age: 30, Email: "[email protected]"}

	data, err := json.Marshal(p)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(data))

	var p2 Person
	if err := json.Unmarshal([]byte(`{"name":"Bob","age":25}`), &p2); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%+v\n", p2)
}

Output:

{"name":"Alice","age":30,"email":"[email protected]"}
{Name:Bob Age:25 Email:}

Common Tag Types

JSON Tags

Control how struct fields appear in JSON output. Use json:"-" to exclude fields and omitempty to skip zero-value fields.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Product struct {
	ID       int     `json:"id"`
	Name     string  `json:"name"`
	Price    float64 `json:"price"`
	Hidden   string  `json:"-"`                     // Ignored
	Optional string  `json:"optional,omitempty"`   // Omitted if empty
}

func main() {
	p := Product{
		ID:     1,
		Name:   "Widget",
		Price:  9.99,
		Hidden: "secret",
	}

	data, err := json.MarshalIndent(p, "", "  ")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(data))
}

Output:

{
  "id": 1,
  "name": "Widget",
  "price": 9.99
}

For more on JSON handling, see Working with JSON in Go.

Database Tags

ORM libraries and database mappers use struct tags to map struct fields to database columns:

package main

import (
	"fmt"
)

type User struct {
	ID    int    `db:"id" json:"id"`
	Name  string `db:"name" json:"name"`
	Email string `db:"email" json:"email"`
	Age   int    `db:"age" json:"age"`
}

func main() {
	u := User{ID: 1, Name: "Alice", Email: "[email protected]", Age: 30}
	fmt.Printf("%+v\n", u)
}

Validation Tags

Validation libraries like go-playground/validator use tags to define field validation rules:

package main

import (
	"fmt"
)

type Account struct {
	Username string `validate:"required,min=3,max=20"`
	Email    string `validate:"required,email"`
	Age      int    `validate:"required,min=18,max=120"`
}

func main() {
	a := Account{
		Username: "alice",
		Email:    "[email protected]",
		Age:      30,
	}
	fmt.Printf("%+v\n", a)
}

Parsing Tags

Extract and use tag information at runtime using the reflect package:

package main

import (
	"fmt"
	"reflect"
	"strings"
)

type Person struct {
	Name  string `json:"name" validate:"required"`
	Age   int    `json:"age" validate:"required,min=0,max=150"`
	Email string `json:"email" validate:"email"`
}

func parseTags(v interface{}) {
	t := reflect.TypeOf(v)

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		jsonTag := field.Tag.Get("json")
		validateTag := field.Tag.Get("validate")

		fmt.Printf("Field: %s\n", field.Name)
		fmt.Printf("  JSON: %s\n", jsonTag)
		fmt.Printf("  Validate: %s\n", validateTag)

		if validateTag != "" {
			rules := strings.Split(validateTag, ",")
			fmt.Printf("  Rules: %v\n", rules)
		}
	}
}

func main() {
	p := Person{}
	parseTags(p)
}

Output:

Field: Name
  JSON: name
  Validate: required
  Rules: [required]
Field: Age
  JSON: age
  Validate: required,min=0,max=150
  Rules: [required min=0 max=150]
Field: Email
  JSON: email
  Validate: email
  Rules: [email]

reflect.TypeOf inspects the struct’s type information, and field.Tag.Get("key") returns the value for a specific tag key. If a tag key is missing, it returns an empty string.

Custom Tag Handling

Create and use custom tags for domain-specific metadata like configuration defaults:

package main

import (
	"fmt"
	"reflect"
)

type Config struct {
	Host    string `config:"host" default:"localhost"`
	Port    int    `config:"port" default:"8080"`
	Debug   bool   `config:"debug" default:"false"`
	Timeout int    `config:"timeout" default:"30"`
}

func loadConfig(v interface{}) map[string]interface{} {
	result := make(map[string]interface{})
	t := reflect.TypeOf(v)

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		configName := field.Tag.Get("config")
		defaultVal := field.Tag.Get("default")

		if configName != "" {
			result[configName] = defaultVal
		}
	}

	return result
}

func main() {
	cfg := Config{}
	config := loadConfig(cfg)

	for key, val := range config {
		fmt.Printf("%s: %v\n", key, val)
	}
}

This pattern lets you build custom configuration loaders, form mappers, or serialization layers without external libraries.

Tag-Based Validation

Validate struct fields at runtime by parsing validation rules from struct tags:

package main

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"
)

type User struct {
	Name  string `validate:"required,min=3,max=50"`
	Age   int    `validate:"required,min=0,max=150"`
	Email string `validate:"required"`
}

func validate(v interface{}) []string {
	var errors []string
	t := reflect.TypeOf(v)
	val := reflect.ValueOf(v)

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		fieldVal := val.Field(i)

		validateTag := field.Tag.Get("validate")
		if validateTag == "" {
			continue
		}

		rules := strings.Split(validateTag, ",")

		for _, rule := range rules {
			parts := strings.Split(rule, "=")
			ruleName := parts[0]

			switch ruleName {
			case "required":
				if fieldVal.IsZero() {
					errors = append(errors, fmt.Sprintf("%s is required", field.Name))
				}
			case "min":
				if len(parts) > 1 {
					minVal, _ := strconv.Atoi(parts[1])
					if fieldVal.Kind() == reflect.Int && fieldVal.Int() < int64(minVal) {
						errors = append(errors, fmt.Sprintf("%s must be >= %d", field.Name, minVal))
					}
				}
			case "max":
				if len(parts) > 1 {
					maxVal, _ := strconv.Atoi(parts[1])
					if fieldVal.Kind() == reflect.Int && fieldVal.Int() > int64(maxVal) {
						errors = append(errors, fmt.Sprintf("%s must be <= %d", field.Name, maxVal))
					}
				}
			}
		}
	}

	return errors
}

func main() {
	u := User{Name: "Al", Age: 200}

	errors := validate(u)
	for _, err := range errors {
		fmt.Println("Error:", err)
	}
}

Output:

Error: Name must be >= 3
Error: Age must be <= 150
Error: Email is required

This is a simplified validation engine — production code should use libraries like go-playground/validator which handle nested structs, custom types, and internationalization.

Multiple Tags

Use multiple tags on a single field for different concerns (JSON serialization, database mapping, CSV export):

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"reflect"
)

type Product struct {
	ID    int     `json:"id" db:"product_id" csv:"id"`
	Name  string  `json:"name" db:"product_name" csv:"name"`
	Price float64 `json:"price" db:"product_price" csv:"price"`
}

func extractTags(v interface{}) {
	t := reflect.TypeOf(v)

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		fmt.Printf("Field: %s\n", field.Name)
		fmt.Printf("  JSON: %s\n", field.Tag.Get("json"))
		fmt.Printf("  DB: %s\n", field.Tag.Get("db"))
		fmt.Printf("  CSV: %s\n", field.Tag.Get("csv"))
	}
}

func main() {
	p := Product{}
	extractTags(p)

	p = Product{ID: 1, Name: "Widget", Price: 9.99}
	data, err := json.Marshal(p)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("JSON:", string(data))
}

Best Practices

Well-Structured Tags

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Article struct {
	ID        int    `json:"id" db:"id" csv:"id"`
	Title     string `json:"title" db:"title" csv:"title" validate:"required,min=5"`
	Content   string `json:"content" db:"content" csv:"content" validate:"required"`
	Author    string `json:"author" db:"author" csv:"author"`
	Published bool   `json:"published" db:"published" csv:"published"`
}

func main() {
	a := Article{
		ID:        1,
		Title:     "Go Struct Tags",
		Content:   "Learn about struct tags...",
		Author:    "Alice",
		Published: true,
	}

	data, err := json.MarshalIndent(a, "", "  ")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(data))
}

Avoid: Inconsistent Tag Naming

// BAD: inconsistent naming across tags — "id" vs "ID" vs "ID"
type User struct {
	ID    int    `json:"id" db:"user_id" csv:"ID"`
	Name  string `json:"name" db:"user_name" csv:"NAME"`
	Email string `json:"email" db:"user_email" csv:"EMAIL"`
}

Mixing snake_case, PascalCase, and UPPER_CASE across tags for the same field makes code harder to maintain and can cause subtle mapping bugs when switching between serialization formats.

Common Tag Conventions

package main

type Example struct {
	// JSON serialization
	Field1 string `json:"field1"`

	// Database mapping
	Field2 string `db:"field_2"`

	// CSV export
	Field3 string `csv:"field3"`

	// Validation rules
	Field4 string `validate:"required,email"`

	// XML serialization
	Field5 string `xml:"field5"`

	// YAML serialization
	Field6 string `yaml:"field6"`

	// Ignored fields in serialization
	Field7 string `json:"-" db:"-"`
}

Best Practices

  1. Use Standard Tags: Prefer json, db, xml, yaml — avoid inventing custom tag formats when standard ones exist.
  2. Be Consistent: Use the same naming convention across all tags for a given field (e.g., always snake_case).
  3. Document Custom Tags: Explain the meaning and expected values of any custom tags you introduce.
  4. Validate Tags: Check tag syntax at startup or in tests to catch typos early.
  5. Cache Reflection: Parse tags once and cache results — don’t re-parse on every request.
  6. Use omitempty: For optional JSON fields to keep output clean.
  7. Handle Errors: Always check errors from serialization functions.
  8. Test Tags: Write unit tests that exercise tag-based behavior for each struct.

Common Pitfalls

  • Typos in Tags: json:"name" vs json:"nme" — no compile-time check, use tests.
  • Inconsistent Naming: Mixing conventions across tags makes maintenance harder.
  • Ignoring Errors: Not checking return values from json.Marshal or json.Unmarshal.
  • Performance: Parsing tags with reflect on every request — cache the results.
  • Over-customization: Too many custom tag keys create opaque behavior — prefer standard tags.

Resources

Summary

Struct tags provide metadata for serialization, validation, and database mapping. Use standard tags like json, db, and xml. Parse tags using reflection when needed. Keep tags consistent and well-documented. Cache tag parsing results for performance.

Comments

Share this article

Scan to read on mobile