Struct Tags

A struct tag is a string on a field that libraries read by reflection to map it to a JSON key, a database column or a rule.

SyntaxName string `json:"name,omitempty" db:"user_name"`

A struct tag is a short string attached to a field. The compiler ignores what it says. Libraries read it while the program runs to decide what the field is called outside your program: the key in JSON, the column in a database table, the rule a validator should apply.

How a tag is written

The tag goes after the field's type, written as a string in backticks. Backticks are used because the tag itself contains double quotes. The language allows any string there. Every library expects the same convention: a space-separated list of key:"value" pairs, as in `json:"user_name" db:"user_name"`. Each library looks up its own key and ignores the rest, so one field can describe itself to several libraries at once.

How a tag is read

Libraries read tags by reflection, which means code inspecting types and values while the program runs rather than at compile time. reflect.TypeOf(User{}) gives the type, FieldByName gives one field, and that field's Tag has two useful calls. Get("json") returns the value for that key, or an empty string if it is absent. Lookup("json") also returns a boolean saying whether the key was there, which separates absent from present-but-empty.

What trips people up

A tag is just a string, so a mistake in one is not a compile error. Straight quotes instead of backticks, a comma between pairs, a space after the colon, a misspelled key: each leaves the library seeing no tag and falling back to the field name. Run go vet, which checks struct tags for exactly these mistakes. A tag on a lowercase field is wasted too, because encoders skip unexported fields entirely.

Example

Example · go
package main

import (
	"fmt"
	"reflect"
)

// One field can carry a key for each library that reads it.
type User struct {
	Name string `json:"name" db:"user_name"`
	Note string `json:"-"`
}

func main() {
	t := reflect.TypeOf(User{})
	f, ok := t.FieldByName("Name")
	if !ok {
		fmt.Println("no field called Name")
		return
	}

	fmt.Println(f.Tag.Get("json"))
	fmt.Println(f.Tag.Get("db"))

	// An absent key gives an empty string from Get.
	fmt.Printf("%q\n", f.Tag.Get("missing"))

	// Lookup also says whether the key was present at all.
	v, present := f.Tag.Lookup("missing")
	fmt.Printf("%q %t\n", v, present)
}

// Output:
// name
// user_name
// ""
// "" false

When to use it

  • Expose lowercase JSON keys while keeping exported Go field names.
  • Keep a secret or internal field out of every encoded response.
  • Map a struct field to a differently named database column for a scanning library.
  • Attach validation rules next to the field they apply to.

More examples

Tags that change JSON output

This is the tag most people meet first. The name part renames the key, so Go keeps capitalised field names while the API stays lowercase. Plan is empty and carries omitempty, so it disappears. Token is tagged "-", so it never leaves the process, which is the usual way to keep a secret out of a response body.

Example · go
package main

import (
	"encoding/json"
	"fmt"
)

type Account struct {
	ID    int    `json:"id"`
	Email string `json:"email"`
	Plan  string `json:"plan,omitempty"`
	Token string `json:"-"`
}

func main() {
	a := Account{ID: 3, Email: "[email protected]", Token: "secret"}
	b, err := json.Marshal(a)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(b))
}

// Output:
// {"id":3,"email":"[email protected]"}

Three tags, two of them broken

Field A has a space after the colon and field B has a comma where the colon belongs. Both compile, and both give an empty value when a library asks for the json key, so the library falls back to the Go field name and your JSON keys come out capitalised. There is no error to search for, which is why go vet is worth running in your build.

Example · go
package main

import (
	"fmt"
	"reflect"
)

type Broken struct {
	A string `json: "a"`
	B string `json,"b"`
	C string `json:"c"`
}

func main() {
	t := reflect.TypeOf(Broken{})
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		fmt.Printf("%s %q\n", f.Name, f.Tag.Get("json"))
	}
}

// Output:
// A ""
// B ""
// C "c"

One struct describing itself to three libraries

Each library reads only its own key, so one declaration can serve the HTTP layer, the database layer and the validator. That keeps the three names for one piece of data in one place. The cost is that a tag grows long, and nothing checks that the db key matches a real column until the query runs.

Example · go
package main

import (
	"fmt"
	"reflect"
)

// json renames the API key, db names the column, validate carries a rule.
type Signup struct {
	Email string `json:"email" db:"email_address" validate:"required,email"`
}

func main() {
	f, ok := reflect.TypeOf(Signup{}).FieldByName("Email")
	if !ok {
		fmt.Println("no field called Email")
		return
	}
	for _, key := range []string{"json", "db", "validate"} {
		fmt.Printf("%s=%s\n", key, f.Tag.Get(key))
	}
}

// Output:
// json=email
// db=email_address
// validate=required,email

Discussion

  • Be the first to comment on this lesson.