Question

Decode HTTP JSON Responses in Go with json.Unmarshal

gojson

Question

How can I read JSON data returned by http.Get in Go? The following program makes a request, but unmarshalling appears to produce an empty result. What is wrong with the struct definitions or JSON decoding code?

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
)

type Tracks struct {
	Toptracks []Toptracks_info
}

type Toptracks_info struct {
	Track []Track_info
	Attr  []Attr_info
}

type Track_info struct {
	Name       string
	Duration   string
	Listeners  string
	Mbid       string
	Url        string
	Streamable []Streamable_info
	Artist     []Artist_info
	Attr       []Track_attr_info
}

type Attr_info struct {
	Country    string
	Page       string
	PerPage    string
	TotalPages string
	Total      string
}

type Streamable_info struct {
	Text      string
	Fulltrack string
}

type Artist_info struct {
	Name string
	Mbid string
	Url  string
}

type Track_attr_info struct {
	Rank string
}

func get_content() {
	url := "http://ws.audioscrobbler.com/2.0/?method=geo.gettoptracks&api_key=c1572082105bd40d247836b5c1819623&format=json&country=Netherlands"

	res, err := http.Get(url)
	if err != nil {
		panic(err.Error())
	}

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		panic(err.Error())
	}

	var data Tracks
	json.Unmarshal(body, &data)
	fmt.Printf("Results: %v\n", data)
	os.Exit(0)
}

func main() {
	get_content()
}

Short Answer

You will learn how Go maps JSON into structs after an HTTP request. The key is to make each Go field match the JSON value type: JSON objects map to structs, JSON arrays map to slices, and unusual JSON keys such as @attr need struct tags. You will also learn to check HTTP and JSON errors instead of silently ignoring them.

Concept

Go's encoding/json package decodes JSON by comparing JSON object keys with exported struct fields or explicit JSON tags.

The most important rule is that the Go type must match the JSON value's shape:

  • A JSON object ({ ... }) maps to a Go struct.
  • A JSON array ([ ... ]) maps to a Go slice such as []Track.
  • A JSON string maps to string.
  • A JSON number maps to a numeric Go type, unless the API sends it in quotes, in which case it is a string.

For this response shape, the outer JSON is similar to:

{
  "toptracks": {
    "track": [
      {
        "name": "Example track",
        "artist": { "name": "Example artist" },
        "@attr": { "rank": "1" }
      }
    ],
    "@attr": { "country": "Netherlands" }
  }
}

Therefore, toptracks must be a single struct, not []ToptracksInfo. Likewise, each track's artist, streamable, and @attr values are objects, so they should be structs rather than slices.

JSON fields named @attr cannot be represented by ordinary Go field-name matching. Use a struct tag such as json:"@attr" to tell the decoder exactly which JSON key belongs to the field.

Finally, always handle the error returned by json.Unmarshal or json.NewDecoder(...).Decode(...). Ignoring it can make a type mismatch look like an empty result.

Mental Model

Think of JSON as a collection of containers:

  • {} is one labeled box, so use one struct.
  • [] is a row of boxes, so use a slice.
  • A value like "name": "Song" is an item inside a box, so use a field such as Name string.

If you tell Go that a single box is a row of boxes—for example, Toptracks []ToptracksInfo when JSON contains "toptracks": { ... }—Go cannot place the data correctly. Decoding returns an error, and if that error is ignored, the result appears empty.

Struct tags are like translation labels. The Go field Attr is readable Go code, while the tag json:"@attr" tells Go that it corresponds to the JSON label @attr.

Take Quiz

Syntax and Examples

Use http.Get to make a request, close the response body, verify the status, and decode JSON directly from the body.

resp, err := http.Get(url)
if err != nil {
	return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
	return fmt.Errorf("unexpected status: %s", resp.Status)
}

var result Response
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
	return err
}

A struct should follow the JSON shape:

type Response struct {
	TopTracks TopTracks `json:"toptracks"`
}

type TopTracks struct {
	Tracks []Track `json:"track"`
	Attr   Attr    `json:"@attr"`
}

type Track struct {
	Name   string `json:"name"`
	Artist Artist `json:"artist"`
}

type Artist struct {
	Name string `json:"name"`
}

type Attr struct {
	Country string `json:"country"`
}

Tracks []Track is a slice because track is an array. TopTracks, Artist, and Attr are individual structs because their JSON values are objects.

Using json.NewDecoder is convenient for an HTTP response because it decodes directly from the response stream. json.Unmarshal is also valid when you already have JSON stored in a []byte.

Step by Step Execution

Consider this JSON:

{
  "toptracks": {
    "track": [
      { "name": "First Song", "artist": { "name": "The Band" } }
    ],
    "@attr": { "country": "Netherlands" }
  }
}

And these types:

type Response struct {
	TopTracks TopTracks `json:"toptracks"`
}

type TopTracks struct {
	Tracks []Track `json:"track"`
	Attr   Attr    `json:"@attr"`
}

type Track struct {
	Name   string `json:"name"`
	Artist Artist `json:"artist"`
}

type Artist struct {
	Name string `json:"name"`
}

type Attr struct {
	Country string `json:"country"`
}

When Go decodes the JSON:

  1. The root object has a toptracks key, so it fills Response.TopTracks.
  2. toptracks contains track, whose value is [...], so Go creates a []Track slice.
  3. The first array item is an object, so it fills one Track value.
  4. name fills Track.Name with "First Song".
  5. artist is an object, so Go fills Track.Artist.Name with "The Band".
  6. The @attr key matches the explicit tag on TopTracks.Attr, filling Attr.Country.

After decoding, you can read values normally:

fmt.Println(result.TopTracks.Attr.Country)    // Netherlands
fmt.Println(result.TopTracks.Tracks[0].Name)  // First Song

Real World Use Cases

JSON HTTP decoding is used whenever a Go program communicates with a web service, including:

  • Calling a weather, maps, payment, or music API.
  • Reading data from an internal microservice.
  • Loading GitHub issues, repository details, or CI results.
  • Downloading application configuration from a service.
  • Building a command-line tool that fetches and displays remote data.
  • Processing webhook payloads in an HTTP server.

In every case, inspect a sample response first, then define only the struct fields your program actually needs. Go safely ignores JSON fields that are not present in your struct.

Take Quiz

Real Codebase Usage

In production code, developers usually wrap API calls in a function that returns typed data and an error:

func FetchTopTracks(client *http.Client, url string) (Response, error) {
	resp, err := client.Get(url)
	if err != nil {
		return Response{}, fmt.Errorf("request top tracks: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return Response{}, fmt.Errorf("request top tracks: %s", resp.Status)
	}

	var data Response
	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
		return Response{}, fmt.Errorf("decode top tracks response: %w", err)
	}

	return data, nil
}

Common project patterns include:

  • Dependency injection: accept *http.Client instead of always using http.Get. This makes timeouts and tests easier.
  • Guard clauses: return early when the request, status code, or decoding fails.
  • Minimal response models: define only fields the application uses.
  • Error context: wrap errors with what the application was trying to do.
  • Timeouts: configure an HTTP client timeout so a stalled server does not block forever.
  • Validation: check that required decoded fields, such as a track name or ID, are not empty before using them.

Common Mistakes

Using a slice for a JSON object

This is incorrect when toptracks is { ... }:

type Response struct {
	TopTracks []TopTracks `json:"toptracks"` // wrong
}

Use one struct instead:

type Response struct {
	TopTracks TopTracks `json:"toptracks"`
}

Ignoring decode errors

This hides the useful explanation for a type mismatch:

json.Unmarshal(body, &data) // error discarded

Always check it:

if err := json.Unmarshal(body, &data); err != nil {
	return err
}

Forgetting to close the response body

Not closing resp.Body can prevent HTTP connections from being reused efficiently.

resp, err := http.Get(url)
if err != nil {
	return err
}
defer resp.Body.Close()

Forgetting special-key tags

A field named Attr does not automatically match @attr.

type TopTracks struct {
	Attr Attr // will not match "@attr"
}

Use a tag:

type TopTracks struct {
	Attr Attr `json:"@attr"`
}

Using unexported fields

The JSON package only fills exported fields, which begin with an uppercase letter.

type Track struct {
	name string // ignored
}

Use Name string instead.

Assuming every API response is successful JSON

An API may return HTML, plain text, or a different JSON error object for a failed request. Check resp.StatusCode before decoding the expected success format.

Comparisons

SituationJSON shapeAppropriate Go type
One nested record"artist": { "name": "A" }Artist
Many records"track": [{...}, {...}][]Track
Text value"name": "Song"string
Special key"@attr": { ... }Attr \json:"@attr"``
Optional nested objectmay be null or {...}*Artist when you need to distinguish null from an empty object

json.Unmarshal and json.Decoder.Decode both decode JSON into Go values:

MethodBest whenInput
json.UnmarshalYou already have all JSON bytes[]byte
json.NewDecoder(r).DecodeReading from an HTTP body or filean io.Reader

For an HTTP response, Decoder.Decode avoids the extra step of reading the entire body into a byte slice first.

Take Quiz

Cheat Sheet

// Request and decode JSON from an HTTP response.
resp, err := http.Get(url)
if err != nil {
	return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
	return fmt.Errorf("unexpected status: %s", resp.Status)
}

var data Response
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
	return err
}
  • JSON object {} → Go struct.
  • JSON array [] → Go slice ([]T).
  • JSON string → string.
  • Struct fields must be exported: Name, not name.
  • Use tags for exact, snake_case, or special JSON keys: json:"@attr".
  • Check errors from http.Get, reading, and JSON decoding.
  • Call defer resp.Body.Close() immediately after a successful request.
  • Unknown JSON fields are ignored by default.
  • Missing JSON fields leave Go fields at their zero values, such as "", 0, false, or nil.

FAQ

Why is my unmarshalled Go struct empty?

A common cause is ignoring an error from json.Unmarshal or Decode. Print or return that error. It often reports that Go expected an array but received an object, or the reverse.

Do Go struct field names have to exactly match JSON keys?

Not always. Go matches exported field names case-insensitively in many ordinary cases. Use explicit json tags for reliable mappings, especially for names like total_pages, fulltrack, or @attr.

Should I use json.Unmarshal or json.NewDecoder for HTTP responses?

Use json.NewDecoder(resp.Body).Decode(&value) for most HTTP responses. Use json.Unmarshal when you specifically need the full JSON body as []byte first.

Why do I need defer resp.Body.Close()?

It releases the response body and lets Go's HTTP transport reuse the underlying connection. Add it after confirming the request returned a non-nil response without an error.

Can I omit fields from my Go JSON struct?

Yes. Go ignores JSON properties that have no corresponding struct field. This is useful when an API response is large but your program needs only a few values.

What type should I use when an API sometimes returns null?

Use a pointer for an optional object, such as *Artist. A nil pointer means the JSON value was missing or null; a non-nil pointer means an object was supplied.

Why does @attr need a JSON tag?

@attr is not a normal Go identifier and does not correspond to the field name Attr automatically. The tag json:"@attr" provides the exact mapping.

Take Quiz

Mini Project

Description

Build a small command-line program that requests a JSON list of public users and prints each user's name and email address. It practices the complete workflow: making an HTTP request, checking the status code, closing the body, decoding an array, and using the resulting structs.

Goal

Fetch a JSON API response and print a readable list of user names and email addresses.

Requirements

Use http.Get to request https://jsonplaceholder.typicode.com/users. Check that the response status is 200 OK. Close the response body with defer. Decode the JSON array into a slice of structs. Print one name <email> line for every user.

Take Quiz

Keep learning

Related questions

View more