Validating REST input fields using golang structs
Add to bookmarksSun Jun 09 2019
Laravel has it's inbuilt validation library, Node has validation.js. What does golang have? A validator library from go-playground that allows the validation of structs and individual fields.
The library offers a whole amazing list of features to rival and surpass libraries like validation.js. Some features include cross-struct validation, nested validations and more.
What are we going to build
We will be building a test api with a single endpoint that mocks user registration but makes use of go-playground's validator to validate all input and give custom validation errors
TLDR: Repo for this code is here
Prerequisites
-Good understanding of the golang language
Project setup
Begin by creating a new project/folder outside your $GOPATH, you can call it govalidator. Then, at the root of the project run this to initialize your project as a go module:
$ go mod init github.com/<username>/govalidator
Next, create a main.go file where your server would start from:
package main
func main(){
}
Setting up the server
We will be using mux as our server handler. Install it by running go get -u github.com/gorilla/mux
at the root of the project.
Now, update the main.go file with these codes:
package main
import (
"fmt"
"github.com/gorilla/mux"
"net/http"
)
func main(){
//Create a new mux router
r := mux.NewRouter()
//Base path to show server works
r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("Hello World!"))
})
//Handle the register path that allows POST methods only
r.HandleFunc("/register",RegisterUser).Methods("POST")
//Use the handler
fmt.Println("Server starting...")
http.ListenAndServe(":3000",r)
}
func RegisterUser(writer http.ResponseWriter, request *http.Request) {
//TODO implement server
}
Now if you run go build
in the project and run the built binary, your server should have started with the message
Server starting...
Now if you navigate to localhost:3000 on your local machine, it should display "Hello World!"
Implementing Register Route
To implement the registration route and validate the input we would be going through a few steps:
- Receive the input as JSON
- Use a string to decode the JSON being parsed
- Validate fields in the string
- Set custom error messages and return error responses if failed
Recieve The Input
Now we need our RegisterUser
handler to actually handle the call to the server. Update your main.go file with this where necessary:
...
import (
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"net/http"
)
...
func RegisterUser(writer http.ResponseWriter, request *http.Request) {
decoder := json.NewDecoder(request.Body)
input := make(map[string]interface{})
err := decoder.Decode(&input)
//Could not decode json
if err != nil{
ErrorResponse(http.StatusUnprocessableEntity,"Invalid JSON",writer)
return
}
//TODO validate json and use successresponse
writer.Write([]byte("Email is "+ input["email"].(string)))
}
func SuccessRespond(fields map[string]interface{}, writer http.ResponseWriter){
fields["status"] = "success"
message,err := json.Marshal(fields)
if err != nil{
//An error occurred processing the json
writer.WriteHeader(http.StatusInternalServerError)
writer.Write([]byte("An error occured internally"))
}
//Send header, status code and output to writer
writer.Header().Set("Content-Type","application/json")
writer.WriteHeader(http.StatusOK)
writer.Write(message)
}
func ErrorResponse(statusCode int, error string, writer http.ResponseWriter){
//Create a new map and fill it
fields := make(map[string]interface{})
fields["status"] = "error"
fields["message"] = error
message,err := json.Marshal(fields)
if err != nil{
//An error occurred processing the json
writer.WriteHeader(http.StatusInternalServerError)
writer.Write([]byte("An error occured internally"))
}
//Send header, status code and output to writer
writer.Header().Set("Content-Type","application/json")
writer.WriteHeader(statusCode)
writer.Write(message)
}
Now the comments are pretty self-explanatory, but what this does is:
- Receive input
- Parse and decode input into a map (to be changed later)
- Give error response if failed
- Display email if it worked
Add User Registration Input Struct
In this section, we are going to be using a struct to decode and validate our input.
Firstly, install the go-validator package with:
$ go get gopkg.in/go-playground/validator.v9
Next, Add a models/user.go file to hold the structs related to the user e.g User struct, Use:
package models
type User struct{
ID uint
Name string `validate:"email"`
Email string `validate:"required,email"`
Password string `validate:"required"`
}
//Embed the user struct properties
type RegisterUserInput struct{
User
ConfirmPassword string `json:"confirm_password" validate:"required"`
}
Implementing the validation method
Go ahead and add the following method to the main.go file to handle validation:
package main
import (
...
"gopkg.in/go-playground/validator.v9"
"reflect"
"strings"
)
var validate *validator.Validate
func main(){
//Make it global for caching
validate = validator.New()
...
}
func validateInputs(dataSet interface{}) (bool,map[string][]string){
err := validate.Struct(dataSet)
if err != nil {
//Validation syntax is invalid
if err,ok := err.(*validator.InvalidValidationError);ok{
panic(err)
}
//Validation errors occurred
errors := make(map[string][]string)
//Use reflector to reverse engineer struct
reflected := reflect.ValueOf(dataSet)
for _,err := range err.(validator.ValidationErrors){
// Attempt to find field by name and get json tag name
field,_ := reflected.Type().FieldByName(err.StructField())
var name string
//If json tag doesn't exist, use lower case of name
if name = field.Tag.Get("json"); name == ""{
name = strings.ToLower(err.StructField())
}
switch err.Tag() {
case "required":
errors[name] = append(errors[name], "The "+name+" is required")
break
case "email":
errors[name] = append(errors[name], "The "+name+" should be a valid email")
break
case "eqfield":
errors[name] = append(errors[name], "The "+name+" should be equal to the "+err.Param())
break
default:
errors[name] = append(errors[name], "The "+name+" is invalid")
break
}
}
return false,errors
}
return true,nil
}
What this method simply does is:
- Use global validation object to validate the passed struct
- If there is an error, check if it a validation syntax error
- If it isn't then it is bound to be an array of field error
- Iterate through the array and generate messages for desired method types by extracting struct names using a reflector
Using the validation method
Now you have a method that receives a struc and performs validation on it, then returns the errors if there are any.
Go ahead and update your main.go file like so:
...
var validate *validator.Validate
...
func RegisterUser(writer http.ResponseWriter, request *http.Request) {
decoder := json.NewDecoder(request.Body)
var input models.RegisterUserInput
err := decoder.Decode(&input)
//Could not decode json
if err != nil {
ErrorResponse(http.StatusUnprocessableEntity, "Invalid JSON", writer)
return
}
if ok, errors := validateInputs(input); !ok {
ValidationResponse(errors, writer)
return
}
response := make(map[string]interface{})
response["message"] = "Email is " + input.Email
SuccessRespond(response,writer)
}
...
func ValidationResponse(fields map[string][]string, writer http.ResponseWriter) {
//Create a new map and fill it
response := make(map[string]interface{})
response["status"] = "error"
response["message"] = "validation error"
response["errors"] = fields
message, err := json.Marshal(response)
if err != nil {
//An error occurred processing the json
writer.WriteHeader(http.StatusInternalServerError)
writer.Write([]byte("An error occured internally"))
}
//Send header, status code and output to writer
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusUnprocessableEntity)
writer.Write(message)
}
Testing
Open up a tool like postman to test your api. Now, if you navigate to localhost:3000/register with the payload
{
"name": "Kofo Okesola",
"email": "[email protected]",
"password": "testpass",
"confirm_password": "testpass"
}
You should see this response
{
"message": "Email is okesolakofo@gmail.com",
"status": "success"
}
Whereas, if you change the payload to something like:
{
"name": "Kofo Okesola",
"email": "okesolakofogmail.com",
"password": "testpass",
"confirm_password": "testpassa"
}
You should be faced with this error:
{
"errors": {
"confirm_password": [
"The confirm_password should be equal to the Password"
],
"email": [
"The email should be a valid email"
]
},
"message": "validation error",
"status": "error"
}
Conclusion
Using golang validator which comes prepackaged with a huge assortment of rules, you should be able to perform any type of validation on your inputs using structs. Think of it as statically typed REST (kind of like Grahpql). You can try playing around with the rules and try generating your own messages.
See you in the next tutorial.
CHEERS!