Validating REST input fields using golang structs

Validating REST input fields using golang structs

Add to bookmarks

Sun 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 [email protected]",
    "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!