Bridging the gap between strict Go types and messy JSON data.
Chapter 17: The ID Card
The rain had stopped, but the archive was colder than usual. Ethan sat at his desk, wrapped in a thick wool sweater, staring at a terminal full of empty brackets.
"It compiles," he muttered. "But it's empty."
Eleanor walked by, carrying a stack of microfiche. "What is empty?"
"My config loader. I'm trying to parse this JSON file from the legacy system. The file has data, the code runs without error, but my Go struct comes out blank. Zero values everywhere."
He pointed to the screen.
The JSON File (config.json):
{
"server_host": "localhost",
"server_port": 8080,
"timeout_ms": 5000
}
Ethan's Code:
type Config struct {
serverHost string
serverPort int
timeoutMs int
}
func loadConfig() {
data := []byte(`{"server_host": "localhost", "server_port": 8080}`)
var cfg Config
// The parsing looks successful...
if err := json.Unmarshal(data, &cfg); err != nil {
log.Fatal(err)
}
// ...but the result is empty
fmt.Printf("%+v\n", cfg)
// Output: {serverHost: "" serverPort:0 timeoutMs:0}
}
"Ah," Eleanor said, glancing at the struct definition. "The Privacy Wall."
The Rule of Visibility
"Go is strict about visibility," Eleanor explained. "If a field starts with a lowercase letter, it is private. It is visible only inside your package."
"I know that," Ethan said. "But I'm using it right here in main."
"You are," she corrected, "but json.Unmarshal is not. That function lives in the encoding/json package. When you pass &cfg to it, it uses Reflection to inspect your struct. But it cannot see your private fields. To the JSON decoder, your struct looks completely empty."
"So I just capitalize them?"
"Start there."
Ethan updated the struct:
type Config struct {
ServerHost string
ServerPort int
TimeoutMs int
}
He ran it again. The output changed:
{ServerHost:"" ServerPort:0 TimeoutMs:0}
"Still empty," Ethan sighed. "Why?"
The Struct Tag
"Because Go is literal about names," Eleanor said. "Your struct field is ServerHost (PascalCase). The JSON field is server_host (snake_case). Go will try to match cases, but it cannot bridge the gap between Host and _host automatically."
"So I have to rename my Go fields to use underscores? Server_host? That looks ugly."
"No," Eleanor said firmly. "Never break Go naming conventions to satisfy an external format. Instead, we use a Tag."
She reached over and typed backticks next to the fields.
type Config struct {
ServerHost string `json:"server_host"`
ServerPort int `json:"server_port"`
TimeoutMs int `json:"timeout_ms"`
}
"Think of a tag as a sticky note attached to the field definition," she explained. "It tells the encoding/json package: 'I know this field is named ServerHost, but when you look at the JSON, look for server_host instead.'"
Ethan ran the code again.
{ServerHost:"localhost" ServerPort:8080 TimeoutMs:0}
"It works," he smiled. "It's like mapping wires."
The "OmitEmpty" Trick
"One more thing," Ethan asked. "What if a field is missing in the JSON? Or I want to hide a field when I write JSON back out?"
"You use options," Eleanor said. "You can stack them inside the tag string."
She modified the timeout field.
type Config struct {
ServerHost string `json:"server_host"`
ServerPort int `json:"server_port"`
TimeoutMs int `json:"timeout_ms,omitempty"` // Added option
Password string `json:"-"` // The "Ignore" tag
LegacyID int `json:"id,string"` // Handles JSON like "id": "123"
}
"I added a few special instructions here," she pointed out.
-
omitempty: "IfTimeoutMsis zero (the default), it won't appear in the JSON output at all. It keeps your payloads clean." -
"-": "This dash tells the encoder to ignore the field entirely. Useful for sensitive data like passwords." -
,string: "Sometimes legacy APIs send numbers as strings, like"id": "123". This tag tells Go to peel off the quotes and parse it as an integer automatically."
The Reflection Magic
Ethan looked at the backticks. "It feels a bit... magical. For a language that hates magic."
"It is the one place Go allows runtime inspection," Eleanor admitted. "Under the hood, json.Unmarshal inspects the memory layout of your struct, reads these tags, and maps the data dynamically. It is slower than writing manual parsing code, but infinitely more convenient."
She stood up to adjust the thermostat.
"Data from the outside world is messy, Ethan. It uses different casing, different structures, different rules. Struct tags are how we keep our internal code clean while still talking to the messy world outside. We don't change our identity; we just wear a name tag."
Key Concepts from Chapter 17
Public vs. Private Fields:
The encoding/json package can only read and write Exported fields (fields starting with a Capital Letter). Lowercase fields are invisible to the parser and will remain empty.
Struct Tags (json:"name"):
Metadata attached to a field definition. They allow you to map Go's PascalCase field names to JSON's snake_case keys without breaking Go naming conventions.
json.Unmarshal (Decoding):
Parses JSON data into a Go struct. It ignores JSON fields that don't match any struct fields (safe partial parsing).
json.Marshal (Encoding):
Converts a Go struct into a JSON string.
Tag Options:
-
omitempty: If the field has the zero value (0, "", nil), it is omitted from the JSON output. -
"-": The field is completely ignored by the JSON encoder/decoder. -
,string: Forces the decoder to parse a string value ("123") into a numeric field (123).
Type Safety:
Go validates types during parsing. If the JSON has a string "8080" but your struct expects an int (and you didn't use the ,string tag), Unmarshal will return a type error.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (2)
The “privacy wall” explanation clicked instantly — I’ve definitely hit the “it unmarshals but everything is zero” moment before and wondered what I broke 😅
I also love how you frame struct tags as a way to keep Go clean while still dealing with messy external data. Super clear and beginner-friendly, even for a topic that usually feels a bit magical.
🙏💯❤