25.2 Struct Tags: json:"name,omitempty" and json:"-"
Right, let’s talk about struct tags. You’ve probably seen these little string literals clinging to your struct fields like metadata remoras. They look like magic incantations, and honestly, they kind of are. The encoding/json package uses them to figure out how to map your beautifully named Go struct fields to the often-absurdly named keys in the JSON you’re marshaling or unmarshaling. Without them, you’re at the mercy of the encoder’s default behavior, which is about as subtle as a brick.
By default, the JSON encoder will use your struct field’s name, just lowercased. So MyField becomes "myfield". This is fine until you need to work with JSON from, say, a Python service that uses snake_case or, heaven forbid, a legacy system that decided PascalCase was a good idea for JSON keys (it wasn’t). This is where the json tag earns its keep.
type User struct {
ID int `json:"id"` // Becomes "id"
FirstName string `json:"first_name"` // Becomes "first_name"
Username string // Becomes "username" (default behavior)
}
The omitempty Directive: Hiding Your Zero-Valued Shame
Sometimes you don’t want to send every field. If a field is its zero value, you might want to just leave it out of the JSON entirely to keep things clean. Enter omitempty. It tells the encoder: “If this field is empty (0 for numbers, "” for strings, false for bools, nil for pointers/slices/maps/channels/functions/interfaces), just pretend it doesn’t exist."
type Profile struct {
Username string `json:"username"`
Nickname string `json:"nickname,omitempty"` // Omitted if empty string
Followers int `json:"followers,omitempty"` // Omitted if 0
}
user := Profile{Username: "gopher"}
jsonBytes, _ := json.Marshal(user)
// {"username":"gopher"} // No 'nickname' or 'followers' in sight.
Watch out: omitempty has a famously weird interaction with time.Time. Because time.Time is a struct, its zero value isn’t nil, it’s just a struct representing January 1, year 1. So the encoder will happily marshal it, often resulting in "0001-01-01T00:00:00Z" in your JSON. To truly omit a time field, you need to use a *time.Time (a pointer) instead, as the zero value for a pointer is nil, which omitempty correctly omits.
The Ultimate Ignore: json:"-"
What if you have a field in your struct that is purely for internal application state and should never be written to or read from JSON? You don’t want some sensitive internal ID or a cached value accidentally leaking into your API response. This is what json:"-" is for. The minus sign is the universal signal to the JSON package to completely ignore this field’s existence.
type Account struct {
PublicID string `json:"id"`
Email string `json:"email"`
authToken string `json:"auth_token"` // Unexported, already safe.
InternalID int `json:"-"` // Exported, but explicitly ignored.
}
acc := Account{PublicID: "abc", Email: "a@b.com", InternalID: 999}
jsonBytes, _ := json.Marshal(acc)
// {"id":"abc","email":"a@b.com"} // InternalID is nowhere to be found.
This is infinitely better than trying to omitempty it or making it unexported (which you should also do for truly internal fields, by the way). It’s a clear, explicit instruction: this is not your data.
Unmarshaling: The Other Half of the Battle
Here’s the beautiful part: these tags work in both directions. When you json.Unmarshal data into a struct, the package uses the same tags to map the incoming JSON keys back to your struct fields. This means you can have a struct that seamlessly round-trips data from a JSON API that uses a completely different naming convention.
A crucial pitfall to avoid: if an incoming JSON object has a key that your struct doesn’t have a tag for (or doesn’t have the field at all), the unmarshaler will just ignore it. It won’t error out. This is usually what you want, but it means you won’t get notified of typos in your JSON or of new fields added by the API that you might want to handle.
A Quick Word for slog
Now, you might be wondering if the new log/slog package uses these same tags. The answer is a resounding no, and thank goodness for that. Slog’s Attr and LogValuer interfaces are a much more powerful and intentional mechanism for controlling logging output. Trying to repurpose json tags for logging would have been a horrible design mistake. The json tag is for JSON. Slog is for structured logs, which might be JSON, but might also be text. The concerns are separate, and the Go team rightly kept them that way.