AlgoMaster Logo

Struct Tags

Last Updated: May 17, 2026

10 min read

A struct tag is a string written right after a field declaration that carries metadata about the field. Go itself ignores tags at compile time; libraries read them at runtime through reflection to decide how to handle each field. JSON encoders use them to rename fields, database libraries use them to map fields to columns, validators use them to enforce rules. This lesson covers what tags are, the exact syntax, the conventional keys you'll meet in real code, and how to read tags yourself with one small reflect call.

What a Struct Tag Is

When you write a struct field, you can attach a string literal after the type. That string is the field's tag. The compiler stores it next to the field in the type information and otherwise leaves it alone. The string only becomes meaningful when some library at runtime asks for it.

The program prints fine, and the tags played no part. The struct works exactly like any struct with three fields. If you delete every tag, the code still compiles and runs the same way. Tags are pure metadata: a place to write notes that other code can read later.

What makes tags useful is that the encoding/json package, the database/sql extensions, validator libraries, YAML parsers, and many other tools all agree to look in this same place. Instead of inventing a separate config file or a registration call for every field, you write the rules directly on the field. The field and its rules travel together.

Here's the picture. The struct definition is the source of truth, and several libraries can read different keys off the same field at runtime.

Each library only looks at the key it owns. JSON ignores the db key, the database library ignores the json key, and so on. That's the design: one field, many readers, no conflicts.

Tag Syntax

The tag is a single string literal placed after the field's type. It must be a string literal, and in practice it's almost always a raw string literal using backticks. Backticks let you include double quotes inside the tag without escaping every one of them.

The convention everyone follows is space-separated key:"value" pairs:

  • The key is a short lowercase identifier (json, xml, db, validate, yaml).
  • A colon goes right after the key, with no space.
  • The value is wrapped in double quotes.
  • Multiple pairs are separated by a single space, not a comma and not multiple spaces.

That single tag string has three key/value pairs. JSON reads the json key and gets "email". The validator reads the validate key and gets "required,email". The database library reads the db key and gets "email_address". Each library splits its own value further (, is the common separator inside a value), but that's library-specific, not part of the tag syntax.

The Go spec is actually quite relaxed about what a tag string can contain. It just has to be a string literal. The key:"value" convention is what the standard library and the broader ecosystem agreed on; it's not a language rule. But you should follow it, because every tool that reads tags assumes that format.

Here's what the same tag looks like as a regular double-quoted string. It works, but you have to escape every internal quote, which is why nobody writes them this way:

Backticks make the same thing readable:

Stick with backticks. Every codebase you'll work with uses them.

Common Keys You'll See

A handful of keys come up in almost every Go codebase. You don't need to memorize every option, just know what each key is for and where to look up its full syntax.

json for Encoding

The encoding/json package looks at the json key to decide how to encode and decode a field. The most common form is just a name, which renames the field in the output. You can add omitempty to skip the field when it holds its zero value.

The output uses id, name, price as keys, not ID, Name, Price. The Discount field has a zero value (0.0) and the omitempty option told JSON to leave it out entirely. Without omitempty, you'd see "discount":0 in the output, which is misleading because the product doesn't actually have a discount, the field was just never set.

xml for XML Encoding

The encoding/xml package uses the xml key. It accepts a name like json does, plus a few qualifiers such as attr (encode this field as an XML attribute instead of an element) and chardata (use it as the element's text content).

The id,attr tag put the order ID on the <order> tag itself as an attribute. The other fields became child elements. This pattern, name,modifier, is the same shape JSON uses for name,omitempty. Different libraries reuse the same ,modifier convention because it's simple to parse.

db for Database Columns

Many database libraries (sqlx, pgx's scan helpers, gorm to some extent) use the db key to map struct fields to database columns. Go convention says struct fields are CamelCase; SQL convention says columns are snake_case. The db tag bridges the two.

The example doesn't actually run a query (that needs a real database driver), but the shape is exactly what you'd see in a real codebase. A library like sqlx calls db.Get(&c, "SELECT customer_id, first_name, last_name, email_address FROM customers WHERE customer_id = ?", 42) and the db tags tell it which column populates which field.

validate for Validation Libraries

Validation libraries like go-playground/validator use the validate key to declare rules for each field. Comma-separated rules are the convention inside the value.

The struct is invalid by every rule on every field, but Go doesn't know that. A validator library, given this struct, would walk the fields, read the validate tag on each, and apply the rules: Username is too short, Email isn't an email, Age is below the minimum. The library is what enforces the rules; the tag is just the declaration.

yaml for YAML

YAML parsers (the popular gopkg.in/yaml.v3 for example) use the yaml key. Same shape as the json tag, just a different key name.

If you've ever worked with a Go service that loaded config from a YAML file, this is what was happening behind the scenes. The fields you saw in config.yaml came from these tags.

Here's a one-line summary of the common keys and where each comes from:

KeyLibraryUsed For
jsonencoding/json (std lib)Renaming fields in JSON, omitting empty values
xmlencoding/xml (std lib)Renaming, choosing element vs attribute
dbsqlx, pgx, othersMapping fields to SQL columns
validatego-playground/validatorField-level validation rules
yamlgopkg.in/yaml.v3Field names in YAML files

You'll meet more (bson for MongoDB, form for HTML form parsing, env for environment-variable loaders), but they all follow the same pattern: a short key name, a value that's a comma-separated list, and one library that reads it.

Putting Tags on an E-Commerce Struct

Here's a more realistic example: a User struct that has to be encoded as JSON, scanned from a database row, and validated when a signup request comes in. One struct, three sets of tags, three libraries that each read what they need.

A few things to notice. The JSON output uses the names from the json key (first_name, last_name, email_address... wait, no: email). The db value email_address only affects the database library; JSON didn't see it. The Phone field has omitempty and was never set, so it's missing from the output entirely. If you had set u.Phone = "555-1234", it would have appeared as "phone":"555-1234".

This is the everyday view of struct tags: one struct, multiple representations, each library reading what it owns. You don't have to write a separate translator function for each external format. The struct is the contract; the tags are the rules.

Reading Tags Yourself

You'll rarely need to read tags directly; the libraries that consume them have already done it. But it's worth seeing the mechanism, because then "library reads the tag" stops being magic.

The reflect package can pull tags off a struct type at runtime. The pattern is: get the struct's type, look up a field by index, ask for the tag, and call .Get(key) on it.

reflect.TypeOf returns a reflect.Type describing the struct. t.NumField() is how many fields it has. t.Field(i) returns a reflect.StructField for the field at index i, which has a Name and a Tag. The tag has a Get method that takes a key name and returns the value for that key, or the empty string if no such key exists.

Notice that Tag.Get("validate") returns "" for the ID field. That's the convention for "no value": there's no error, no panic, just an empty string. Library code typically checks if tag != "" to decide whether the field opted in.

The reflect package is a deep topic in its own right, but for tags this is essentially the whole API. The same pattern works on any struct type: get the type, walk the fields, ask each field for the tags you care about.

Common Mistakes

Tags look harmless, but the syntax is unforgiving. A few mistakes show up repeatedly.

Forgetting the Backticks

Without backticks (or any string literal at all), the line isn't a tag, it's a syntax error. The compiler catches this immediately, so you'll see it the moment you save.

The compiler rejects this with something like:

The fix is to wrap the tag in backticks: ` json:"name" `.

Wrong Quoting Around the Value

The value half of key:"value" must use double quotes. Single quotes don't work, and missing quotes don't work either.

This compiles (it's a valid string literal), but reflect.Tag.Get("json") returns the empty string, because the value isn't double-quoted. The standard library's tag parser sees no valid key:"value" pair and reports "no such key". The bug then shows up as JSON encoding using the original field name (Name) instead of the one you tried to specify (name), which can be confusing because everything else looks fine.

The fix is to remember the double quotes:

Multiple Spaces Between Pairs

The convention is exactly one space between key:"value" pairs. Many tag parsers, including the one in reflect, are strict: extra spaces, tabs, or no space at all will cause one or both keys to be ignored.

Two spaces between the pairs. Depending on which Go version and library version you're on, the second key may or may not be readable. The safe, portable form is one space:

Mismatched Value Formats per Library

Each library defines its own grammar inside the value. The JSON tag uses commas to separate modifiers: "name,omitempty". The validate tag uses commas to separate rules: "required,email,min=3". They look similar, but they're not interchangeable.

Putting validator-style rules inside the json value does nothing. JSON sees email as the name and treats required,email as unknown modifiers it silently ignores. The email field will be encoded as "email": ... and that's it, no validation happens. The fix is to give each library its own key:

Now JSON gets a clean "email" and the validator gets required,email to enforce.

Capitalising the Key

Tag keys are lowercase by convention. json, db, xml, validate, yaml. Writing JSON:"name" or DB:"customer_id" won't be recognised by the standard library's encoders, because they call Tag.Get("json") and Tag.Get("db") with lowercase keys.

This compiles, but json.Marshal will output the field as "Name" (the original field name), not "name". The custom case JSON is technically stored, but no consumer is looking for it. Stick with lowercase.

Unexported Fields with Tags

Unexported fields (lowercase first letter) are invisible to most reflection-based libraries, regardless of what tags they have. The encoding/json package, for example, only encodes exported fields. A tag on an unexported field is purely decorative.

JSON encoding of this struct will output {"total":...} only. The id field is invisible because it's unexported. If you want it in the JSON output, you have to capitalize it: ID int.

Summary

  • A struct tag is a string literal placed after a field's type, holding metadata that libraries read at runtime through reflection.
  • The standard convention is backtick-quoted, space-separated key:"value" pairs. One space between pairs, no space around the colon, double quotes around values.
  • The common keys are json, xml, db, validate, and yaml. Each is owned by a different library or family of libraries, and each library reads only its own key.
  • omitempty (in JSON) and ,attr (in XML) follow a shared shape: a value, a comma, then a modifier. Other libraries use the same pattern for their own options.
  • You can read tags yourself with reflect.TypeOf(x).Field(i).Tag.Get("json"). Unknown keys return the empty string. Reflection has cost, so cache the results when you reflect in hot paths.
  • Tags on unexported fields are visible through reflection but are usually skipped by encoders, which only operate on exported fields.
  • Common mistakes are missing backticks, missing or wrong quotes around values, multiple spaces between pairs, capitalised keys, and cramming rules from one library into another's value.

In the next lesson, Constructor Functions, we'll look at the Go idiom for creating struct instances with validation and sensible defaults, the closest Go gets to a "constructor" without inventing one in the language.