Skip to Content
Course content

49: Encoding and Decoding with encoding/json

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Almost every Go service you write will eventually need to talk to the outside world, and in 99% of cases, that conversation happens in JSON. The encoding/json package is the standard tool for this, but there is a massive gap between "making it work" and "making it maintainable."

The convenience of map[string]any

When I first started with Go, I treated JSON like I did in JavaScript. I just wanted the data, and I didn't want to spend time defining a rigid structure for a response that might change. The naive approach is to unmarshal your JSON into a map[string]any (or map[string]interface{} in older versions of Go). It feels liberating because you don't have to define a struct; you just throw the bytes at a map and start digging.

// The "I'm in a hurry" approach
data := []byte(`{"city": "Seattle", "temp": 52.5, "conditions": "Rainy"}`)
var result map[string]any

if err := json.Unmarshal(data, &result); err != nil {
    log.Fatal(err)
}

// Now comes the pain...
city := result["city"].(string) 
temp := result["temp"].(float64)

This works fine for a tiny script, but it's a nightmare in a production codebase. Notice that type assertion .(string)? That's a ticking time bomb. If the API suddenly returns a null or an integer where you expected a string, your entire program panics. Plus, you lose all the benefits of Go's type system. You're essentially writing dynamically typed code inside a statically typed language, which defeats the purpose of using Go in the first place.

The type-safety of Structs

The better way—the way you should do it for any real feature—is to define a struct that mirrors the shape of the JSON. By doing this, you move the validation to the Unmarshal step. If the data doesn't fit the struct, you get an error immediately rather than a panic deep in your business logic three function calls later.

type WeatherResponse struct {
    City       string  `json:"city"`
    Temperature float64 `json:"temp"`
    Conditions  string  `json:"conditions"`
}

var weather WeatherResponse
if err := json.Unmarshal(data, &weather); err != nil {
    log.Fatal(err)
}

fmt.Printf("It is %f degrees in %s", weather.Temperature, weather.City)

Now, the compiler knows exactly what weather.Temperature is. No assertions, no guessing, and your IDE can actually autocomplete the fields for you. It's a bit more boilerplate up front, but it saves you from hours of debugging "interface conversion" panics.

Handling the gaps with JSON tags

You'll notice those backticked strings like `json:"city"`. These are struct tags, and they are where the real power lies. JSON keys are usually camelCase or snake_case, but Go requires fields to be PascalCase (exported) to be visible to the json package. Tags bridge that gap.

I often see developers struggle when an API returns a field that is sometimes missing. If you use a standard type, Go will just fill it with the zero value (like an empty string or 0). If you need to distinguish between "the API sent a 0" and "the API didn't send the field at all," you should use a pointer.

type UserProfile struct {
    Username string  `json:"username"`
    Bio      *string `json:"bio,omitempty"` // Pointer handles null/missing
}

The omitempty tag is another lifesaver. When you're encoding (converting a struct back to JSON), omitempty tells Go to leave the key out of the resulting JSON string if the value is empty. This keeps your API responses clean and prevents you from sending a bunch of "field": null values to your frontend clients.




📋 Practical Task

Build a GitHub Issue Parser

You need to write a small utility that consumes a JSON payload representing a GitHub issue. The payload contains nested data and some fields that might be missing.

Requirements:

  • Create a struct Issue that maps to the JSON provided below.
  • The user field in the JSON is a nested object; create a separate User struct to handle this.
  • The body of the issue is optional. Use a pointer and the omitempty tag so that if the body is empty, it doesn't appear when you encode the struct back to JSON.
  • Unmarshal the provided JSON string into your structs, then print the User's login and the Issue title.
// Use this input for your test:
jsonInput := `{"title": "Bug: Login fails on Safari", "body": "Happens every time I click submit", "user": {"login": "octocat", "id": 1}}`
Rating
0 0

There are no comments for now.

to be the first to leave a comment.