Encoding structs
In this chapter we’re going to revisit 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:
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:
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:
There are a few interesting things in this response to point out:
Our
Moviestruct has been encoded into a single JSON object, with the field names and values as the key-value pairs.By default, the keys in the JSON object are equal to the field names in the struct (
ID,CreatedAt,Titleand so on). We’ll talk about how to customize these shortly.If a struct field doesn’t have an explicit value set, then the JSON-encoding of the zero value for the field will appear in the output. We can see an example of this in the response above — we didn’t set a value for the
Yearfield in our Go code, but it still appears in the JSON output with the value0.
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:
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:
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 omitzero 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 omitzero directive hides a field in the JSON output if and only if the value equals the zero value of the field’s type. As a reminder, here are the zero values for the Go types that can be encoded to JSON:
| Go type | Zero value |
|---|---|
bool |
false |
string |
"" |
int*, uint*, float*, rune |
0 |
| slices, maps, pointers | nil |
| structs | each field in the struct has its zero value |
| arrays | each element in the array is set to the zero value for its type |
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 omitzero directive to hide the Year, Runtime and Genres fields in the output if and only if they are the relevant zero value for their type.
Go ahead and update the struct tags like so:
package data ... type Movie struct { ID int64 `json:"id"` CreatedAt time.Time `json:"-"` // Use the - directive Title string `json:"title"` Year int32 `json:"year,omitzero"` // Add the omitzero directive Runtime int32 `json:"runtime,omitzero"` // Add the omitzero directive Genres []string `json:"genres,omitzero"` // Add the omitzero 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:
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 omitzero directive. The other fields that we used omitzero on (Runtime and Genres) are unaffected.
Additional information
The omitempty directive
The omitzero directive that we’ve used in this chapter was a new addition in Go 1.24, and it significantly overlaps in behavior with another — much older — directive called omitempty.
The omitzero directive is very clear and consistent about what it does — it omits something from the JSON when the value exactly matches the zero value for the type. And that’s the only thing it does.
Whereas the omitempty directive is similar to omitzero but less consistent in its behavior. It differs from omitzero in a few important ways:
omitemptywill not omit structs, even if all the struct fields have their zero value.omitemptywill not omittime.Timetypes, even if they have their zero value. Note that this is because thetime.Timetype is actually a struct behind-the-scenes, so this is really just a special case of the bullet point above.omitemptywill not omit arrays, even if they have their zero value.omitemptywill omit empty slices and maps (that is, initialized slices and maps with length zero) as well asnilslices and maps.
Now that omitzero exists, the only time I would generally recommend using the omitempty directive is when you want to omit empty slices or maps from your JSON entirely, instead of having them encode to an empty JSON array like [].
For example, if you wanted to completely omit the Genres field from the JSON whenever it contains no values (or is nil), you could use the omitempty directive like so:
type Movie struct { ID int64 `json:"id"` CreatedAt time.Time `json:"-"` Title string `json:"title"` Year int32 `json:"year,omitzero"` Runtime int32 `json:"runtime,omitzero"` Genres []string `json:"genres,omitempty"` // Use the omitempty directive Version int32 `json:"version"` }
The string 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,omitzero"` Runtime int32 `json:"runtime,omitzero,string"` // Add the string directive Genres []string `json:"genres,omitzero"` 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.