Menu Close

Validate REST Input in Go

Implementing REST APIs is a typical use case for Go applications. Malformed data that got accepted by your API, can lead to critical errors in other parts of your system. The best-case scenario is that your database has some mechanism in place to prevent malformed data from being stored. When it does not, this data can lead to errors and unexpected behavior in your customer-facing applications. In this post, we’re going to cover, how to validate data sent to a REST API in Go.

Example REST API

This is a simple example of a REST API, build with the gorilla/mux package. It is a great HTTP router, particularly for REST APIs. The API provides one endpoint with the path /user. To keep it simple, it only accepts an HTTP GET for all users and an HTTP Post to create a user. Additionally, it has no persistent database but stores the users in-memory with a slice. 

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"strings"

	"github.com/gorilla/mux"
)

type User struct {
	ID                 int
	FirstName          string
	LastName           string
	FavouriteVideoGame string
	Email               string
}

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/user", PostUser).Methods(http.MethodPost)
	router.HandleFunc("/user", GetUsers).Methods(http.MethodGet)

	log.Fatal(http.ListenAndServe(":8081", router))
}

var users = []User{}
var id = 0

func validateEmail(email string) bool {
	// This is obviously not a good validation strategy for email addresses
	// pretend a complex regex here
	return !strings.Contains(email, "@")
}

func PostUser(w http.ResponseWriter, r *http.Request) {
	user := User{}
	json.NewDecoder(r.Body).Decode(&user)

	// We don't want an API user to set the ID manually
	// in a production use case this could be an automatically
	// ID in the database
	user.ID = id
	id++

	users = append(users, user)
	w.WriteHeader(http.StatusCreated)
}

func GetUsers(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(users); err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}

Now let’s see how we can manually validate input given in a request body to the POST handler of this API.

Validate Input manually

Let’s say we want to set FirstName, LastName, and Email as required when creating a user with the Post handler. Furthermore, we want the Email field to be a valid email address. A simple approach is to validate the fields manually like this:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/gorilla/mux"
)

type User struct {
	ID                 int
	FirstName          string
	LastName           string
	FavouriteVideoGame string
	Email               string
}

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/user", PostUser).Methods(http.MethodPost)
	router.HandleFunc("/user", GetUsers).Methods(http.MethodGet)

	log.Fatal(http.ListenAndServe(":8081", router))
}

var users = []User{}
var id = 0

func validateEmail(email string) bool {
	// That's obviously not a good validation strategy for email addresses
	// pretend a complex regex here
	return !strings.Contains(email, "@")
}

func PostUser(w http.ResponseWriter, r *http.Request) {
	user := User{}
	json.NewDecoder(r.Body).Decode(&user)

	errs := []string{}

	if user.FirstName == "" {
		errs = append(errs, fmt.Errorf("Firstname is required").Error())
	}

	if user.LastName == "" {
		errs = append(errs, fmt.Errorf("LastName is required").Error())
	}

	if user.Email == "" || validateEmail(user.Email) {
		errs = append(errs, fmt.Errorf("A valid Email is required").Error())
	}

	if len(errs) > 0 {
		w.Header().Add("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		if err := json.NewEncoder(w).Encode(errs); err != nil {
		}
		return
	}

	// We don't want an API user to set the ID manually
	// in a production use case this could be an automatically
	// ID in the database
	user.ID = id
	id++

	users = append(users, user)
	w.WriteHeader(http.StatusCreated)
}

func GetUsers(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(users); err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}

Notice that we use an error string slice here to gather all validation errors before returning a response to the user. With this method, we can return a detailed error message to inform the API caller of what exactly was wrong with the submitted data.

You can see that this validation method is very verbose. And we have to define a custom function to verify common things like email addresses. Let’s see how we can improve this.

Validate Input with struct tags

A more idiomatic way to validate structs in Go is using struct tags. There are many packages for struct validation by struct tags. We’re going to use https://github.com/go-playground/validator here. This does not only enable us to use struct tags for validation but also provides many predefined validation methods for example for email addresses.

If you need other validators for your data check out the documentation for the validator package. There’s a good chance that the validator you need is under the over 80 validators provided by the package.

package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/go-playground/validator/v10"
	"github.com/gorilla/mux"
)

type User struct {
	ID                 int    `validate:"isdefault"`
	FirstName          string `validate:"required"`
	LastName           string `validate:"required"`
	FavouriteVideoGame string
	Email               string `validate:"required,email"`
}

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/user", PostUser).Methods(http.MethodPost)
	router.HandleFunc("/user", GetUsers).Methods(http.MethodGet)

	log.Fatal(http.ListenAndServe(":8081", router))
}

var users = []User{}
var id = 0

func PostUser(w http.ResponseWriter, r *http.Request) {
	user := User{}
	json.NewDecoder(r.Body).Decode(&user)

	validate := validator.New()

	err := validate.Struct(user)
	if err != nil {
		validationErrors := err.(validator.ValidationErrors)
		w.Header().Add("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		responseBody := map[string]string{"error": validationErrors.Error()}
		if err := json.NewEncoder(w).Encode(responseBody); err != nil {
		}
		return
	}

	// We don't want an API user to set the ID manually
	// in a production use case this could be an automatically
	// ID in the database
	user.ID = id
	id++

	users = append(users, user)
	w.WriteHeader(http.StatusCreated)
}

func GetUsers(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(users); err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}

The validationError.Error() above returns a string that sums up every failing validation in the struct. So the BadRequest response has still very detailed information on what went wrong.

We changed the validation to use the validator package and now validate against the following rules:

  • The ID field should not be set by the user, so we verify that it has the default value of ints, which is 0
  • The FullName and LastName are required
  • The Email field is required and validated with a predefined email validator

Validate Input with custom validator

So now we’re using the validator package and validate the struct with struct tags. But how can we validate a struct field that can not be validated by the provided tags of the library?

Let’s say we want to blacklist certain video games. Because we don’t want users that like games like PUBG or Fortnite in our system. In this case, we are able to define a custom validate tag value and let the validate package use it like this:

First we define a validate function:

func GameBlacklistValidator(f1 validator.FieldLevel) bool {
	gameBlacklist := []string{"PUBG", "Fortnite"}
	game := f1.Field().String()
	for _, g := range gameBlacklist {
		if game == g {
			return false
		}
	}
	return true
}

Then we register the function and a corresponding tag with the validator instance.

...
	validate := validator.New()
	validate.RegisterValidation("game-blacklist", GameBlacklistValidator)
...

And now we add the tag in the definition of the User struct.

type User struct {
	ID                 int    `validate:"isdefault"`
	FirstName          string `validate:"required"`
	LastName           string `validate:"required"`
	FavouriteVideoGame string `validate:"game-blacklist"`
	Email              string `validate:"required,email"`
}

With this we finally blocked PUBG and Fortnite players from our system.

Conclusion

Validating REST API Input is crucial to prevent your application from malformed data. You can write your own validation logic, but in most cases, it is better to use a well-maintained validation package like github.com/go-playground/validator. This enables you to use tags in your structs to configure validation, and keeps the logic to run the validation simple. If you have a special use case that needs uncommon validation functions you can still define your own extensions for the validator package.

Hope this helps you to validate input given to your API

Leave a Reply

Your email address will not be published.