Stage 1 · Code
Standard Library Toolkit
Go Deeper Than Just Saving JSON
JSON stops being simple the moment the shape gets nested, optional, or partly unknown. This lesson shows how Go's `encoding/json` package handles those real-world cases.
Tags and Nested Shapes
In the earlier JSON lesson, the data shape was friendly: a few fields, one struct, not many surprises. Real APIs and config files are usually more layered than that. They contain nested objects, slices of objects, optional fields, and names that do not match Go's exported field names exactly.
Struct tags are the little labels that tell the encoder and decoder how those fields should appear in JSON. They answer questions like 'what should this key be called?' and 'should this field disappear when empty?'.
{
"id": "proj-7",
"title": "Edge Payments",
"owner": {
"name": "Ava"
},
"services": [
{
"name": "api",
"region": "ap-south-1",
"tags": [
"public",
"critical"
],
"internal": false
},
{
"name": "worker",
"region": "eu-west-1",
"internal": true
}
]
}Notice what is absent: owner.email and services[1].tags. omitempty makes the encoder leave zero-value fields out entirely.
Dynamic JSON Shapes
Sometimes you know the overall envelope but not every field inside it. Maybe an event payload changes by event type, or maybe part of the response is user-defined. In those cases, a rigid struct for every possible field can be more awkward than helpful.
| Tool | Use it when | Tradeoff |
|---|---|---|
map[string]any | The JSON object is flexible or partly unknown | Convenient, but you lose compile-time type checking |
json.RawMessage | You want to delay decoding of a specific subsection | Slightly more work, but keeps the raw bytes for a later decision |
If the JSON is even looser than that, map[string]any is the blunt but practical tool. Just remember that the price of flexibility is more type assertions and more manual checking at runtime.
Custom JSON Rules
Sometimes the built-in encoding rules are close but not quite right. Maybe a type should serialize as 2026-07-20 instead of a full RFC3339 timestamp. Maybe a numeric code should appear as a string. When that happens, a type can take control by implementing MarshalJSON and UnmarshalJSON.
Once a type defines its own JSON behavior, other code will rely on that shape. Change it carefully, because you are no longer just changing internals. You are changing the wire format.
Quiz
What does `json:"name,omitempty"` mean on a struct field?
Practice
Write a function `EncodeProfile(name string, tags []string) (string, error)` that marshals a struct to compact JSON with the keys `name` and `tags`. The returned string should exactly match JSON output such as `{"name":"Ava","tags":["go","sre"]}`.
Summary
- Struct tags control JSON key names and omission rules.
- Nested structs and slices map naturally to nested JSON objects and arrays.
map[string]anyandjson.RawMessagehelp when part of the JSON shape is dynamic.- Custom
MarshalJSONandUnmarshalJSONmethods let a type define its own wire format. - A good JSON design is not just about encoding. It is about keeping the round trip predictable.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.