You're reading a sample of this book. Get the full version here.
Let's Go Further Sending JSON Responses › Encoding Structs
Previous · Contents · Next
Chapter 3.3.

Encoding Structs

In this chapter we’re going to head back to the showMovieHandler method that we made earlier and update it to return a JSON response which represents a single movie in our system. Similar to this:

{
    "id": 123,
    "title": "Casablanca",
    "runtime": 102,
    "genres": [
        "drama",
        "romance",
        "war"
    ],
    "version": 1
}

Instead of encoding a map to create this JSON object (like we did in the previous chapter), this time we’re going to encode a custom Movie struct.

So, first things first, we need to begin by defining a custom Movie struct. We’ll do this inside a new internal/data package, which later will grow to encapsulate all the custom data types for our project along with the logic for interacting with our database.

If you’re following along, go ahead and create a new internal/data directory containing a movies.go file:

$ mkdir internal/data
$ touch internal/data/movies.go

And in this new file let’s define the custom Movie struct, like so:

File: internal/data/movies.go
package data

import (
    "time"
)

type Movie struct {
    ID        int64     // Unique integer ID for the movie
    CreatedAt time.Time // Timestamp for when the movie is added to our database
    Title     string    // Movie title
    Year      int32     // Movie release year
    Runtime   int32     // Movie runtime (in minutes)
    Genres    []string  // Slice of genres for the movie (romance, comedy, etc.)
    Version   int32     // The version number starts at 1 and will be incremented each 
                        // time the movie information is updated
}

Now that’s done, let’s update our showMovieHandler to initialize an instance of the Movie struct containing some dummy data, and then send it as a JSON response using our writeJSON() helper.

It’s quite simple in practice:

File: cmd/api/movies.go
package main

import (
    "fmt"
    "net/http"
    "time" // New import

    "greenlight.alexedwards.net/internal/data" // New import
)

...

func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) {
    id, err := app.readIDParam(r)
    if err != nil {
        http.NotFound(w, r)
        return
    }

    // Create a new instance of the Movie struct, containing the ID we extracted from 
    // the URL and some dummy data. Also notice that we deliberately haven't set a
    // value for the Year field.
    movie := data.Movie{
        ID:        id,
        CreatedAt: time.Now(),
        Title:     "Casablanca",
        Runtime:   102,
        Genres:    []string{"drama", "romance", "war"},
        Version:   1,
    }

    // Encode the struct to JSON and send it as the HTTP response.
    err = app.writeJSON(w, http.StatusOK, movie, nil)
    if err != nil {
        app.logger.Error(err.Error())
        http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError)
    }
}

OK, let’s give this a try!

Restart the API and then visit localhost:4000/v1/movies/123 in your browser. You should see a JSON response which looks similar to this:

03.03-01.png

There are a few interesting things in this response to point out:

Changing keys in the JSON object

One of the nice things about encoding structs in Go is that you can customize the JSON by annotating the fields with struct tags.

Probably the most common use of struct tags is to change the key names that appear in the JSON object. This can be useful when your struct field names aren’t appropriate for public-facing responses, or you want to use an alternative casing style in your JSON output.

To illustrate how to do this, let’s annotate our Movies struct with struct tags so that it uses snake_case for the keys instead. Like so:

File: internal/data/movies.go
package data

...

// Annotate the Movie struct with struct tags to control how the keys appear in the 
// JSON-encoded output.
type Movie struct {
    ID        int64     `json:"id"`
    CreatedAt time.Time `json:"created_at"`
    Title     string    `json:"title"`
    Year      int32     `json:"year"`
    Runtime   int32     `json:"runtime"`
    Genres    []string  `json:"genres"`
    Version   int32     `json:"version"`
}

And if you restart the server and visit localhost:4000/v1/movies/123 again, you should now see a response with snake_case keys similar to this:

03.03-02.png

Hiding struct fields in the JSON object

It’s also possible to control the visibility of individual struct fields in the JSON by using the omitempty and - struct tag directives.

The - (hyphen) directive can be used when you never want a particular struct field to appear in the JSON output. This is useful for fields that contain internal system information that isn’t relevant to your users, or sensitive information that you don’t want to expose (like the hash of a password).

In contrast the omitempty directive hides a field in the JSON output if and only if the struct field value is empty, where empty is defined as being:

To demonstrate how to use these directives, let’s make a couple more changes to our Movie struct. The CreatedAt field isn’t relevant to our end users, so let’s always hide this in the output using the - directive. And we’ll also use the omitempty directive to hide the Year, Runtime and Genres fields in the output if and only if they are empty.

Go ahead and update the struct tags like so:

File: internal/data/movies.go
package data

...

type Movie struct {
    ID        int64     `json:"id"`
    CreatedAt time.Time `json:"-"` // Use the - directive
    Title     string    `json:"title"`
    Year      int32     `json:"year,omitempty"`    // Add the omitempty directive
    Runtime   int32     `json:"runtime,omitempty"` // Add the omitempty directive
    Genres    []string  `json:"genres,omitempty"`  // Add the omitempty directive
    Version   int32     `json:"version"`
}

Now when you restart the application and refresh your web browser, you should see a response which looks exactly like this:

03.03-03.png

We can see here that the CreatedAt struct field no longer appears in the JSON at all, and the Year field (which had the value 0) doesn’t appear either thanks to the omitempty directive. The other fields that we used omitempty on (Runtime and Genres) are unaffected.


Additional Information

The string struct tag directive

A final, less-frequently-used, struct tag directive is string. You can use this on individual struct fields to force the data to be represented as a string in the JSON output.

For example, if we wanted the value of our Runtime field to be represented as a JSON string (instead of a number) we could use the string directive like this:

type Movie struct {
    ID        int64     `json:"id"`
    CreatedAt time.Time `json:"-"`
    Title     string    `json:"title"`
    Year      int32     `json:"year,omitempty"`
    Runtime   int32     `json:"runtime,omitempty,string"` // Add the string directive
    Genres    []string  `json:"genres,omitempty"` 
    Version   int32     `json:"version"`
}

And the resulting JSON output would look like this:

{
  "id": 123,
  "title": "Casablanca",
  "runtime": "102",       ← This is now a string
  "genres": [
    "drama",
    "romance",
    "war"
  ],
  "version": 1
}

Note that the string directive will only work on struct fields which have int*, uint*, float* or bool types. For any other type of struct field it will have no effect.