JavaRush /Courses /Go SELF /Struct tags — how to read json:"title,omitempty"

Struct tags — how to read json:"title,omitempty"

Go SELF
Level 21, Lesson 3
Available

1. What is a struct tag and why it is needed

If you are seeing tags in Go for the first time, the feeling is usually: “why did a field suddenly get some string in backticks, and why isn’t the compiler complaining?”. That’s a normal reaction: tags look like magic spells, but in reality they are just a label string attached to a struct field for external conventions.

Imagine a struct as a box with compartments (fields). The compartment names are convenient for us in code: Title, Done, ID. But as soon as we want to “show” this data to the outside world—for example, save it, transmit it, or display it in another format—we sometimes need different names or additional rules. That’s where a tag appears: a “sticker” on the box compartment.

It’s important to pin this down: a tag by itself does nothing. It does not validate data, it does not serialize it, and it does not automatically change the behavior of Go code. It only stores a string that libraries can read (by convention).

A minimal tag example looks like this:


package main

type Task struct {
	Title string `json:"title"`
}

func main() {}

Here the Title field has a tag. It says (by convention, with the json key): “if someone represents this field in JSON, let it be called title”.

2. Where the tag is written and what it looks like

When syntax feels “weird”, it helps to slow down a bit and look at it as language grammar. In Go, a tag is written in the struct field declaration, immediately after the field type. That is, the pattern is: FieldName FieldType Tag.

If you write it visually, you get something like this:

Title   string   `json:"title,omitempty"`
│       │        │
│       │        └─ tag (string literal)
│       └─ field type
└─ field name

The tag itself is a string literal. Most often it’s written in backticks (`), because then you don’t need to escape double quotes inside:

package main

type Task struct {
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

func main() {}

Technically, you can write it in regular quotes too, but then you have to escape:

package main

type Task struct {
	Title string "json:\"title\""
}

func main() {}

This compiles as well, but readability suffers: in a real project, a week later you won’t forgive yourself for this “ASCII art made of slashes”.

3. Tag format: key:"value" and multiple keys

Now the most important part: what exactly is inside that string. By the convention commonly used in Go, a tag usually consists of pairs like key:"value". The key is the “name of the convention”, and the value is the “rules of that convention”.

Inside a single tag, you can store several such pairs, separating them with spaces:

package main

type Task struct {
	Title string `json:"title" db:"task_title"`
}

func main() {}

This reads as: “for JSON this field is called title, and for some database convention (the db convention) it’s task_title”. Again: Go itself does nothing, but a library that understands db:"..." will be able to read this label.

To get used to it, it helps to have a small “decoding table”:

Fragment Meaning
json:"title"
key json, value title
db:"task_title"
key db, value task_title
a space between pairs “these are two different conventions”

Let me emphasize an important point: the space between pairs is required, because this is one string, and the parser must understand the boundaries.

4. How to read json:"title,omitempty"

Now we’re approaching the most common example: json:"title,omitempty". Here “everything is in one string”, and a beginner usually doesn’t understand where the name ends and the option begins.

Let’s break it down step by step:

package main

type Task struct {
	Title string `json:"title,omitempty"`
}

func main() {}

Here:

  • json is the convention key,
  • "title,omitempty" is the value (a string),
  • inside the value:
    • title is the external field name,
    • omitempty is an option.

The key reading trick is: it’s customary to separate parts of the json value with commas. Up to the first comma is the name; after it are options.

If you rewrite it “in words”, you get: “The Title field is called title in JSON. If the field value is empty (equal to the zero value), then it may be omitted from the output”.

5. Practice and useful nuances of json tags

What omitempty means and why zero values keep coming up

The word omitempty is short for “omit empty”, i.e., “skip empty”. But in Go, “empty” is not philosophy and not a mood check; it’s a fairly strict idea: empty = zero value (in the basic sense).

That is, for string the zero value is "", for int it’s 0, for bool it’s false. If a field equals its zero value, then omitempty allows (by JSON convention) not to include the field in the output.

Here’s a small table so you don’t have to keep it in your head “on faith”:

Field type Zero value What counts as “empty” for omitempty
string
""
empty string
int
0
zero
bool
false
false
[]T
nil
nil (and often an empty slice too—depends on the library context)
map[K]V
nil
nil

Why was this invented at all? Because when transmitting/saving data, there is often no point in storing “emptiness” if it is implied by default anyway. For example, it helps reduce size and decreases “noise” in the data.

Important not to confuse it: omitempty is not “the field is optional in the model”, it’s “a convenient external representation when the field is empty”.

Two practical examples with Task

Now we’ll do what any developer typically does: take our Task model and “prepare” it for the outside world. We won’t serialize to JSON in code (that’s a separate topic), but we’ll learn to write tags so they read like a contract.

Let’s start simple: explicit external names.

package main

type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

func main() {}

Now let’s add omitempty for fields that are sometimes convenient to hide when empty. For example, a note (if the user didn’t write anything) or a deadline string (if there isn’t one).

package main

type Task struct {
	ID       int    `json:"id"`
	Title    string `json:"title"`
	Done     bool   `json:"done"`
	Note     string `json:"note,omitempty"`
	Deadline string `json:"deadline,omitempty"`
}

func main() {}

Pay attention to an important bit of “engineering boredom”: we do not add omitempty for Title. Why? Because an empty title is most likely a data error. And if you start “hiding” such fields, the problem becomes less noticeable rather than disappearing. A tag is not treatment; it’s makeup.

Special cases: json:"-" and partial initialization

In real code, you often run into situations where a field exists in a struct, but it must not end up in JSON at all. For example, an internal debugging field or a technical flag. For that, json has a special name - (minus), which is usually read as “ignore this field”.

package main

type Task struct {
	ID       int    `json:"id"`
	Title    string `json:"title"`
	Done     bool   `json:"done"`
	internal string `json:"-"` // the field is unexported and also ignored
}

func main() {}

Here I intentionally made the internal field unexported (lowercase). This fits well with the idea of model boundaries: “this should not be visible from outside”. And the json:"-" tag is extra insurance at the external-format level.

One more useful idea: a tag does not cancel zero values. If you partially initialize a struct, some fields will be at their zero value—and that’s exactly why omitempty sometimes “suddenly” hides a field:

package main

import "fmt"

type Task struct {
	Title string `json:"title"`
	Note  string `json:"note,omitempty"`
}

func main() {
	t := Task{Title: "Read Go spec"}
	fmt.Printf("%+v\n", t) // {Title:Read Go spec Note:}
}

We’re not serializing here, but you can already see: Note is an empty string, i.e., the zero value for string. And that state is exactly what will be “considered empty” for omitempty.

Why tags are not validation

It’s very tempting to think that if the tag says omitempty, then the field is “optional”, and if there is no tag, then it’s “required”. That’s a common logical mistake, because many frameworks in other languages work like this: annotation = rule.

In Go, this is separated much more strictly. A tag is just a label string. Validation is code. So you can create Task{Title: ""} even with json:"title" and even without omitempty. A tag does not forbid it. It forbids nothing at all.

If you keep this in mind, half of the future bugs like “why did empty task titles get into the system, I removed omitempty?!” will disappear. You removed omitempty—you only said “show the empty field too”, not “forbid an empty field”.

6. Common mistakes when working with struct tags

Mistake #1: forgetting the space between pairs key:"value".
When you put several conventions into one tag, it’s easy to accidentally do this: `json:"title"db:"task_title"`. As a result, the string becomes invalid for the expected format, and the library (which tries to read the tag) may “not see” the second part. The fix is boring: add a space and don’t be shy about formatting fields in aligned columns.

Mistake #2: trying to write a tag in regular quotes without escaping.
A beginner writes Title string "json:"title"", and the compiler naturally can’t understand where the string ends. If for some reason you don’t use backticks, then the double quotes inside must be escaped: "json:\"title\""—but it’s better to keep it simple and use backticks.

Mistake #3: believing that omitempty makes a field “optional” in the model.
omitempty only affects the external format (by JSON convention) and usually means “if the field equals the zero value, you may omit it”. This is not a data correctness guarantee and not protection from empty values inside the program. If a field is important for the model (for example, Title), it must be checked in code.

Mistake #4: a typo in the tag key.
jsno:"title" looks almost like json:"title", especially at 2 a.m. The compiler won’t complain, because for Go it’s just a string. But the library that looks for the json key will find nothing and will apply the default behavior. So it’s worth treating tags as part of a public contract: they “break silently”, without alarms and without hints.

Mistake #5: turning a tag into a “dump for everything”.
Sometimes you want to hang everything on a field: json, db, validate, doc, example, xml, yaml, and a couple more personal traumas. Technically you can, but readability drops and the struct turns into a Christmas tree. Good practice is to add tags only where there really is an external contract, and to maintain them carefully as part of your model’s interface.

1
Task
Go SELF, level 21, lesson 3
Locked
Book tags
Book tags
1
Task
Go SELF, level 21, lesson 3
Locked
Double labels
Double labels
1
Task
Go SELF, level 21, lesson 3
Locked
JSON options
JSON options
1
Task
Go SELF, level 21, lesson 3
Locked
Tag rules
Tag rules
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION