The Magic of JSON in Go: Why Are Some Fields Visible and Others Not?
\nHave you ever encountered a situation where your Go struct sends something in the JSON response that the client doesn't need to see? For example, a user's password, an internal database ID, or a secret key. Or, conversely, you need to show a field only under certain conditions (for example, if it's not empty).
\nThis problem is one of the most popular on Stack Overflow, and for good reason. Working with JSON in Go is an art of balancing type strictness with presentation flexibility. Today we'll cover all the methods: from the simple json:"-" tag to complex custom solutions with interfaces. Let's go!
1. The Simplest Way: The json:"-" Tag
\nGo allows you to control struct serialization through special tags. If you want to permanently hide a field from JSON, simply add json:"-".
type User struct {\n Name string `json:"name"`\n Password string `json:"-"`\n Age int `json:"age"`\n}\n\nfunc main() {\n u := User{Name: "Alice", Password: "supersecret", Age: 30}\n data, _ := json.Marshal(u)\n fmt.Println(string(data))\n // Output: {"name":"Alice","age":30}\n}\nThe Password field has disappeared, as if it never existed. Perfect for confidential data that should never leave the server.
2. Conditional Hiding: omitempty
\nSometimes you need to hide a field only if it's empty. For this, the omitempty option is used (short for "omit if empty").
type Product struct {\n Name string `json:"name"`\n Description string `json:"description,omitempty"`\n Price float64 `json:"price,omitempty"`\n}\n\nfunc main() {\n p1 := Product{Name: "Book", Price: 500.0}\n data1, _ := json.Marshal(p1)\n fmt.Println(string(data1)) // {"name":"Book","price":500}\n\n p2 := Product{Name: "Course"}\n data2, _ := json.Marshal(p2)\n fmt.Println(string(data2)) // {"name":"Course"}\n}\nImportant: omitempty only works with "zero" values: "", 0, false, nil (for slices, maps, pointers).
Pitfall with Numeric Fields
\nBe careful: if Price equals 0, it will also be hidden. In some business logic, this is unacceptable (a free product is still a price). In such cases, it's better to use pointers.
3. Using Pointers (*) and Interfaces
\nPointers are your best friend when you need to distinguish between "value not specified" and "value equals zero".
\ntype UserProfile struct {\n Name string `json:"name"`\n Age *int `json:"age,omitempty"` // pointer to int\n Bio *string `json:"bio,omitempty"`\n}\n\nfunc main() {\n // Age not specified — field will be hidden\n u1 := UserProfile{Name: "Ivan"}\n data1, _ := json.Marshal(u1)\n fmt.Println(string(data1)) // {"name":"Ivan"}\n\n // Age = 0 — this is a meaningful value\n zero := 0\n u2 := UserProfile{Name: "Maria", Age: &zero}\n data2, _ := json.Marshal(u2)\n fmt.Println(string(data2)) // {"name":"Maria","age":0}\n}\nThis approach is often used in REST APIs where you need to update only the passed fields (partial updates, PATCH requests).
\n\n4. Custom Serialization: The json.Marshaler Interface
\nIf standard tags are not enough, you can fully control the process. Implement the json.Marshaler interface and write your own logic.
type DynamicUser struct {\n ID int\n Name string\n ShowEmail bool\n Email string\n}\n\nfunc (u DynamicUser) MarshalJSON() ([]byte, error) {\n // Create an anonymous struct only for serialization\n m := map[string]interface{}{\n "id": u.ID,\n "name": u.Name,\n }\n if u.ShowEmail {