JSON and encoding/json

Course 3 · Ch 5
JSON and encoding/json
Converting between Go structs and JSON text — the format almost every real API speaks

Fundamentals Chapter 10 used response.json() in JavaScript without much ceremony — JSON parses directly into a plain object there, since JavaScript objects and JSON already share the same shape. Go's static typing (Fundamentals Chapter 2) means converting to and from JSON needs an explicit step: marshaling (struct → JSON) and unmarshaling (JSON → struct), both handled by the standard library's encoding/json package.

Marshaling — Struct to JSON

import "encoding/json" type Person struct { Name string Age int } p := Person{Name: "Philip", Age: 35} data, err := json.Marshal(p) if err != nil { fmt.Println("Error:", err) } fmt.Println(string(data)) // {"Name":"Philip","Age":35}

json.Marshal returns the JSON as a []byte slice, plus an error — the same Fundamentals Chapter 5 pattern used throughout the language. string(data) converts those raw bytes into a printable string. Note that Name and Age appear capitalised in the JSON output by default — only capitalised (exported) struct fields are visible to encoding/json at all.

Struct Tags — Controlling the JSON Output

type Person struct { Name string `json:"name"` Age int `json:"age"` Email string `json:"email,omitempty"` } p := Person{Name: "Philip", Age: 35} data, _ := json.Marshal(p) fmt.Println(string(data)) // {"name":"Philip","age":35} — Email omitted, it's empty

The backtick-delimited text after each field is a struct tag — metadata read by encoding/json at runtime. json:"name" renames the field for JSON purposes (lowercase, matching typical API conventions); omitempty drops the field entirely from the output when it holds its zero value (Fundamentals Chapter 2) — here, Email disappears since it was never set.

_ in the discarded error
data, _ := json.Marshal(p) uses the blank identifier (Fundamentals Chapter 4) to explicitly throw away the error return value — acceptable in a quick example, but real code should always check it properly, the same way err != nil is checked everywhere else in Go.

Unmarshaling — JSON to Struct

jsonData := []byte(`{"name":"Sam","age":28}`) var p Person err := json.Unmarshal(jsonData, &p) if err != nil { fmt.Println("Error:", err) } fmt.Println(p.Name) // "Sam"

json.Unmarshal goes the other direction: raw JSON bytes into a struct. The destination is passed as a pointer (&p) — exactly Intermediate Chapter 1's pointer-receiver lesson applied here: without the &, Unmarshal would only be able to fill in a throwaway copy, and p back in the caller would remain empty.

Forgetting & before the destination is a silent bug
json.Unmarshal(jsonData, p) (no &) compiles and runs without panicking in many cases, but p never actually gets populated — Unmarshal needs a pointer specifically so it can write through to the real variable, the same plain-vs-pointer-receiver distinction from Intermediate Chapter 1.

Fetching JSON from a Real API

type Post struct { ID int `json:"id"` Title string `json:"title"` } func fetchPost(id int) (Post, error) { var post Post resp, err := http.Get(fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", id)) if err != nil { return post, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return post, err } err = json.Unmarshal(body, &post) return post, err }

This is the complete realistic pattern: http.Get fetches the response, io.ReadAll reads its full body into bytes, json.Unmarshal parses those bytes into a struct. defer resp.Body.Close() (Intermediate Chapter 3's defer) guarantees the response body is closed once the function returns, success or failure — the direct Go equivalent of JavaScript Fundamentals Chapter 10's fetch + response.json() pair.

JavaScriptGo
JSON.stringify(obj)json.Marshal(value)
JSON.parse(text)json.Unmarshal(bytes, &target)
No equivalent — keys match property names`json:"key"` struct tag controls the JSON key name
await response.json()io.ReadAll(resp.Body) + json.Unmarshal(body, &target)

Coding Challenges

Challenge 1

Define a struct Product with fields Name (string), Price (float64), and InStock (bool), with json tags using lowercase key names. Create one, marshal it with json.Marshal, and print the resulting JSON string.

📄 View solution
Challenge 2

Given a raw JSON string `{"name":"Laptop","price":999.99,"inStock":true}`, define a matching struct with appropriate json tags, unmarshal it, and print each field.

📄 View solution
Challenge 3

Write a function fetchUser(id int) (User, error) that fetches from https://jsonplaceholder.typicode.com/users/{id}, unmarshals into a User struct with at least Name and Email fields (with json tags), properly closing the response body with defer. Call it and print the result, handling any error.

📄 View solution

Chapter 5 Quick Reference

  • json.Marshal(value) — struct/value → JSON []byte, plus an error
  • json.Unmarshal(bytes, &target) — JSON []byte → struct; target MUST be a pointer
  • Only capitalised (exported) struct fields are visible to encoding/json
  • `json:"key"` — struct tag controlling the JSON field name
  • `json:"key,omitempty"` — omits the field from output if it's the zero value
  • http.Get(url) + io.ReadAll(resp.Body) + json.Unmarshal — the full fetch-and-parse pattern
  • defer resp.Body.Close() — always close a response body once done with it
  • This completes Go Advanced (Course 3) and the Go course overall as currently planned across all 3 courses.