Go generic wrapper to parse request payload
By Audren Bouëssel du Bourg
When building web api, one probably deals with incoming JSON objects.
JSON is decoded from the http request body into a struct like below :
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
func HandlePostUser(w http.ResponseWriter, r *http.Request) {
var u User
err := json.NewDecoder(r.Body).Decode(&u)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// do what you got to do to save resource
}
Go generics from 1.18 update offers an interesting opportunity to shorten this handler into:
-func HandlePostUser(w http.ResponseWriter, r *http.Request) {
+func HandlePostUser(user User, w http.ResponseWriter, r *http.Request) {
- var u User
- err := json.NewDecoder(r.Body).Decode(&u)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
// do what you got to do to save resource
}
This was already possible before 1.18 with a middleware to parse body into a specific type.
Now, a tiny middleware can get you any
type you may need. See below:
// PayloadHandlerFunc is a custom http.HandlerFunc taking an extra parsed payload
// object as a parameter
type PayloadHandlerFunc[Payload any] func(Payload, http.ResponseWriter, *http.Request)
// HandleJSONPayload unmarshal json body into given generic object,
// before passing to its payload handler PayloadHandlerFunc.
func HandleJSONPayload[Payload any](handle PayloadHandlerFunc[Payload]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var pl Payload
if err := json.NewDecoder(r.Body).Decode(&pl); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
handle(pl, w, r)
}
}
Usage
Here is an example with a simple multiplexer from stdlib.
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
type Address struct {
Line string `json:"line"`
City string `json:"city"`
}
func HandlePostUser(user User, w http.ResponseWriter, r *http.Request) {
// do what you got to do to save user
}
func HandlePostAddress(address Address, w http.ResponseWriter, r *http.Request) {
// do what you got to do to save an address
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", HandleJSONPayload(HandlePostUser))
mux.HandleFunc("/addresses", HandleJSONPayload(HandlePostAddress))
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
Pros/Cons
Less code to read / less code to test / less code to write.
Your handlers don’t have the default http.HandlerFunc
signature.
The generic middleware may be a bit hard to understand at first glance.