Encoding JSON and Struct Tags in Go
Turn Go structs into JSON with json.Marshal, control field names with struct tags, use omitempty and "-", and fix fields that do not show up.
The first JSON surprise in Go is usually an empty object: you marshal a struct and get {}. The second is field names in the wrong case. Both come down to two rules, exported fields and struct tags, and this page covers them along with omitempty.
json.Marshal and Exported Fields
package main
import (
"encoding/json"
"fmt"
)
type user struct {
name string
Email string
}
func main() {
data, err := json.Marshal(user{name: "Asha", Email: "asha@example.com"})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(string(data))
}
{"Email":"asha@example.com"}
name is missing. encoding/json can only see exported fields, the ones that start with a capital letter. Lowercase fields are skipped without any error. If you get {} or missing fields, this is almost always the cause.
Struct Tags Set the JSON Names
Go fields are CamelCase, but most APIs use snake_case or camelCase. A struct tag tells the encoder what name to use:
package main
import (
"encoding/json"
"fmt"
"time"
)
type Order struct {
ID int `json:"id"`
Customer string `json:"customer_name"`
Total float64 `json:"total"`
CreatedAt time.Time `json:"created_at"`
Items []string `json:"items"`
}
func main() {
o := Order{
ID: 42,
Customer: "Ravi",
Total: 19.5,
CreatedAt: time.Date(2026, 3, 1, 10, 30, 0, 0, time.UTC),
Items: []string{"pen", "notebook"},
}
data, _ := json.MarshalIndent(o, "", " ")
fmt.Println(string(data))
}
{
"id": 42,
"customer_name": "Ravi",
"total": 19.5,
"created_at": "2026-03-01T10:30:00Z",
"items": [
"pen",
"notebook"
]
}
The tag goes in backticks after the type. MarshalIndent gives readable output, which is nice for config files and debugging. time.Time is written in RFC 3339 format automatically.
Tag typos fail silently:
json: "id" with a space, or json:id without quotes, is not a valid tag, and the field falls back to its Go name. go vet catches this with a "struct field tag ... not compatible with reflect.StructTag.Get" warning, so run it.
omitempty and "-"
Two tag options cover most needs:
package main
import (
"encoding/json"
"fmt"
)
type Profile struct {
Name string `json:"name"`
Bio string `json:"bio,omitempty"`
Tags []string `json:"tags,omitempty"`
Age int `json:"age,omitempty"`
Password string `json:"-"`
}
func main() {
a, _ := json.Marshal(Profile{Name: "Meera", Password: "secret"})
fmt.Println(string(a))
b, _ := json.Marshal(Profile{Name: "Dev", Bio: "Go dev", Tags: []string{"go"}, Age: 30})
fmt.Println(string(b))
}
{"name":"Meera"}
{"name":"Dev","bio":"Go dev","tags":["go"],"age":30}
omitemptyleaves the field out when it holds the zero value:"",0,false,nil, or an empty slice or map.-means never include this field. Use it for passwords and internal data.
omitempty can hide real zeros:
With omitempty, an Age of 0 disappears from the output, so the reader cannot tell "zero" from "not set". When zero is a valid value, use a pointer like *int. A nil pointer is omitted, while a pointer to 0 is written as 0.
omitempty has no effect on structs such as time.Time, because a struct is never considered empty. Go 1.24 added the omitzero option for that case, which omits a field when it is the zero value of its type, including a zero time.Time.
Nil Slices vs Empty Slices
This catches people writing APIs for JavaScript frontends:
package main
import (
"encoding/json"
"fmt"
)
type Response struct {
Results []string `json:"results"`
}
func main() {
var none []string
a, _ := json.Marshal(Response{Results: none})
b, _ := json.Marshal(Response{Results: []string{}})
fmt.Println(string(a))
fmt.Println(string(b))
}
{"results":null}
{"results":[]}
A nil slice becomes null, and an empty slice becomes []. If your frontend does data.results.length, null crashes it. Initialize the slice with []string{} or make([]string, 0) before encoding when you want [].
Maps and Custom Output
For JSON whose shape you do not know ahead of time, a map[string]any works. Keys are sorted in the output:
package main
import (
"encoding/json"
"fmt"
)
func main() {
data, _ := json.Marshal(map[string]any{
"status": "ok",
"count": 3,
"tags": []string{"a", "b"},
})
fmt.Println(string(data))
}
{"count":3,"status":"ok","tags":["a","b"]}
Prefer structs when you know the fields. They document the format and the compiler catches typos.
You control the JSON shape:
If you can explain why a lowercase field is missing, rename fields with tags, drop empty ones with omitempty, hide secrets with -, and choose between null and [], you can produce any JSON an API needs.
Summary
json.Marshal only encodes exported fields, so capitalize the ones you want. Struct tags like json:"created_at" set the names, omitempty drops zero values, omitzero handles zero structs, and - hides a field completely. Use pointers when zero is meaningful, and remember that a nil slice encodes as null while an empty one encodes as [].