How to Parse JSON Request Body with an Array to Create a Relationship Between Structs

I’m using Go (Golang)/Chi to create a REST API.

This is the structure of the JSON payload from the request body:

{
  "name": "John Smith",
  "bornDate": "2020-10-05",
  "books": [
     {
       "title": "Book 1 Title",
       "publishYear": "1989"
     },
     {
       "title": "Book 2 Title",
       "publishYear": "1999"
     }
   ]
}

And here are the Structs I’m attempting to impart onto this request:

type AuthorRequest struct{
    Name string `json: "name"`
    BornDate string `json: "bornDate"`
    Books []BookRequest `json: "books"`
}

type BookRequest struct{
    Title string `json: "title"`
    PublishYear string `json: "publishYear"`
}

And I’m attempting to save AuthorRequest and BookRequest down as Author Struct and Book Struct.

type Author struct{
    Name string
    BornDate time.Time
    Books []Book
}

type Book struct{
    Title string
    PublishYear int32
}

And this is my handler function for this POST request:

func CreateAuthorAndBooks(w http.ResponseWriter, r *http.Request) {
   // Assume that AuthorRequest is in an api package
   var authorPayload api.AuthorRequest
   decoder := json.NewDecoder(r.Body)
   err := decoder.Decode(&authorPayload)

   if err != nil{
     // Logging an error here on missing fields
   }

   var successMessage string = fmt.Sprintf("%v's has %v books: %v", authorPayload.Name, len(authorPayload.Books), authorPayload.Books)
   var response map[string]string = map[string]string{"success": successMessage}
   w.Header().Set("Content-Type", "application/json")
   
   var err = json.NewEncoder(w).Encode(response)
   if err != nil{
     // Logging an internal server error
   }
  // Logic to create Author and its related Book Structs...
}

But for some reason, when I send the above JSON request body and receive a response, there are 0 books and there is an empty slice [] when referencing books.

How do I properly parse and access an array within a JSON request body for a POST request in Go/Chi?

The issue likely lies in your struct tags for the json package. Specifically, you have extra spaces in your json tags, which causes them to be ignored when unmarshalling the JSON payload. For example, json: "name" should be json:"name". These extra spaces make Go treat the tag as invalid.

Here’s how to fix your code and ensure proper unmarshalling of the JSON payload:

Corrected Structs

type AuthorRequest struct {
    Name     string        `json:"name"`
    BornDate string        `json:"bornDate"`
    Books    []BookRequest `json:"books"`
}

type BookRequest struct {
    Title       string `json:"title"`
    PublishYear string `json:"publishYear"`
}

type Author struct {
    Name     string
    BornDate time.Time
    Books    []Book
}

type Book struct {
    Title       string
    PublishYear int32
}

Updated Handler Function

func CreateAuthorAndBooks(w http.ResponseWriter, r *http.Request) {
    // Decode the JSON payload
    var authorPayload AuthorRequest
    decoder := json.NewDecoder(r.Body)
    err := decoder.Decode(&authorPayload)
    if err != nil {
        http.Error(w, "Invalid request payload", http.StatusBadRequest)
        return
    }

    // Transform AuthorRequest to Author struct
    bornDate, err := time.Parse("2006-01-02", authorPayload.BornDate)
    if err != nil {
        http.Error(w, "Invalid date format", http.StatusBadRequest)
        return
    }

    // Convert Books
    var books []Book
    for _, book := range authorPayload.Books {
        publishYear, err := strconv.Atoi(book.PublishYear)
        if err != nil {
            http.Error(w, "Invalid publishYear format", http.StatusBadRequest)
            return
        }
        books = append(books, Book{
            Title:       book.Title,
            PublishYear: int32(publishYear),
        })
    }

    author := Author{
        Name:     authorPayload.Name,
        BornDate: bornDate,
        Books:    books,
    }

    // Create a success message
    successMessage := fmt.Sprintf("%v has %v books: %v", author.Name, len(author.Books), author.Books)
    response := map[string]string{"success": successMessage}

    // Write the response
    w.Header().Set("Content-Type", "application/json")
    err = json.NewEncoder(w).Encode(response)
    if err != nil {
        http.Error(w, "Failed to encode response", http.StatusInternalServerError)
        return
    }

    // Logic to save `author` into the database can go here...
}

Key Changes

  1. Struct Tags:
  • Removed extra spaces in json tags to make them valid.
  • Use json:"fieldName" instead of json: "fieldName".
  1. Error Handling:
  • Added checks to handle errors when parsing dates (BornDate) and converting PublishYear to an integer.
  1. Transformation:
  • Convert AuthorRequest to Author struct by transforming BornDate to time.Time and PublishYear to int32.
  1. Books Handling:
  • Loop through the Books array in authorPayload and construct Book structs with proper type conversion.

Testing Your API

Send the following JSON payload in a POST request:

{
  "name": "John Smith",
  "bornDate": "2020-10-05",
  "books": [
    {
      "title": "Book 1 Title",
      "publishYear": "1989"
    },
    {
      "title": "Book 2 Title",
      "publishYear": "1999"
    }
  ]
}

The response should correctly include the books:

{
  "success": "John Smith has 2 books: [{Book 1 Title 1989} {Book 2 Title 1999}]"
}

Debugging Tip

If you’re still encountering issues:

  • Use fmt.Printf("%+v\n", authorPayload) right after decoding to check how the payload is parsed into the struct.
  • Ensure the content type of your request is application/json.