Skip to Content
Course content

53: JSON Encoding and Decoding for APIs

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

I want to build a small service that handles a book inventory. In a real-world API, we aren't just moving data around internally; we're talking to front-ends or other services. That means we need to speak JSON. Let's start with the most basic thing: taking a Go struct and turning it into a JSON string.

The Capitalization Conflict

I'll start by defining a Book struct. Since I want the json package to be able to see these fields, I have to export them by capitalizing the first letter. If I don't, the encoder will just ignore them entirely.

package main

import (
	"encoding/json"
	"fmt"
)

type Book struct {
	Title  string
	Author string
	Pages  int
}

func main() {
	myBook := Book{Title: "The Go Programming Language", Author: "Alan Donovan", Pages: 380}
	data, _ := json.Marshal(myBook)
	fmt.Println(string(data))
}

When I run this, I get: {"Title":"The Go Programming Language","Author":"Alan Donovan","Pages":380}. Now, here is the problem. In the Go world, we capitalize for visibility. But in the JSON/JavaScript world, Title (uppercase) is unconventional. Most APIs expect camelCase, like title. I can't just lowercase the struct fields in Go, or the json package won't be able to access them. It's a bit of a catch-22.

Solving the Naming Game

To fix this, I need to tell Go exactly how I want these fields to look when they leave the program. We do this with "struct tags." These are those little strings wrapped in backticks that you've probably seen in a lot of Go code.

type Book struct {
	Title  string `json:"title"`
	Author string `json:"author"`
	Pages  int    `json:"pages"`
}

Now, when I run json.Marshal again, the output is {"title":"The Go Programming Language","author":"Alan Donovan","pages":380}. Perfect. I've decoupled my internal Go naming conventions from my external API contract. I should also mention that if I have a field I want to keep internal—say, a DatabaseID—I just leave it lowercase (databaseID) and the JSON package will ignore it completely.

Decoding the Incoming Request

Encoding is the easy part. The real work happens when a client sends JSON to us. I'll try the most straightforward approach: reading the whole body into a byte slice and then unmarshaling it.

// Imagine this is inside an HTTP handler
var b Book
err := json.Unmarshal(bodyBytes, &b)
if err != nil {
	fmt.Println("Error decoding JSON:", err)
}

This works fine for tiny payloads. But if I'm building a production API, reading the entire request body into memory just to parse it is wasteful. The http.Request.Body is an io.Reader, and the json package gives us a better way to handle streams: json.NewDecoder.

Streaming with NewDecoder

Instead of Unmarshal, I'll use a decoder. This reads directly from the network socket, which is much more memory-efficient for larger requests.

func handleBookUpload(w http.ResponseWriter, r *http.Request) {
	var b Book
	
	// We wrap the request body in a decoder and decode directly into the struct
	err := json.NewDecoder(r.Body).Decode(&b)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	
	fmt.Fprintf(w, "Received book: %s", b.Title)
}

One thing I've noticed when doing this is that if the JSON is missing a field, Go doesn't complain; it just leaves that field as the "zero value" (like an empty string or 0). If you need to distinguish between a field being "missing" and a field being "explicitly set to empty," you'll need to use pointers in your struct (e.g., *string), but for 90% of APIs, the standard zero-value behavior is exactly what you want.




📋 Practical Task

Exercise: Building a Movie Review Parser

You are tasked with creating a tool that processes movie review data sent by a client. The client sends a JSON object representing a movie review, but their naming convention is slightly different from your internal Go naming.

Requirements:

  • Create a struct named MovieReview with the following fields:
    • MovieTitle (mapped to JSON key "movie_title")
    • Rating (mapped to JSON key "star_rating")
    • ReviewerName (mapped to JSON key "reviewer")
    • InternalID (this field should NOT appear in the JSON at all)
  • Write a function that takes a JSON string (as a []byte) and decodes it into your MovieReview struct.
  • Write a second function that takes a MovieReview struct and encodes it back into a JSON string, verifying that the keys match the requirements.
  • Test your code with this JSON input: {"movie_title": "Inception", "star_rating": 5, "reviewer": "Jane Doe"}. Ensure the InternalID remains hidden.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.