How to write validation in Golang?
If you just learning golang maybe it's time to write validation on your http request for your backend application. There's a lot of library that just do that, if you want a quick solution for that you can choose one of these library.
But, if you're just learning Go, I suggest you write your own validation, it's really easy, and you're in the right place. We will explore how to create a validation for HTTP requests in Go.
Writing validation using Struct
Let's design how are going to use the validation. There's many way we can do that, but go is simple language with simple syntact. The most easy way to place validation role is using struct meta tag.
Here's example struct that we will use.
type RequestCreateProject struct {
ID int64 `json:"id,omitempty"`
Name string `json:"name" validate:"required"`
Slug string `json:"slug" validate:"required,slug"`
Description string `json:"description"`
Tags []string `json:"tags"`
}
This is example request body.
PATCH /api/projects
Content-type: application/json
{
"id":null,
"name": "",
"slug": "inside#reporta",
"description": "Marketing tool for reporting campaigns",
"tags":["marketing"]
}
And let's expect the error message would be like this.
{
"errors": [
"ID field is required.",
"Name field is required.",
"Slug Field must be alpha numeric"
],
"status": 422,
"title": "Unprocessable Entity"
}
Go reflect
So now we have the concept for how are going to do the validation, now let's find out how we can implement that in go.
So go have standar library to work with struct it called reflect this library work in runtime reflection it's allow us to manipulate object with arbitrary type, but one thing to remember is we should becareful when using it. Because if there's some error in our code it will not detected by the compiler.
So let's write basic validation.
func ValidateStruct(input interface{}) []error {
errs := []error{}
value := reflect.ValueOf(input)
if value.Kind() == reflect.Ptr {
value = reflect.Indirect(value)
}
for i := 0; i < value.NumField(); i++ {
tag := value.Type().Field(i).Tag.Get(tagName)
if tag == "" || tag == "-" {
continue
}
validator := getValidatorFromTag(tag)
valid, err := validator.Validate(value.Field(i).Interface())
if !valid && err != nil {
field := value.Type().Field(i).Name
errs = append(errs, fmt.Errorf("%s %s", field, err.Error()))
}
}
return errs
}
This function returning array of error since we will processing multiple tag in our validation. Here's reflect function that you need to know.
reflect.ValueOf(input)
is function that we can use to get the reflect instance from the params in this caseinterface{}
- To handle the case when the object is pointer we can use function
reflect.Indirect(value)
and we can get reflect type usingvalue.Kind()
and the reflect object for pointer isreflect.Ptr
the pointer it self come from param when we passing theinput
like thisValidateStruct(&input)
- The struct will have multiple properties so we can get the length of the properties using
value.NumField()
- And to get the tag of the properties
value.Type().Field(i).Tag.Get(tagName)
- The
tagName
is the keywork tag that we use for this validation which is the valuevalidation
see the struct example above.
And here's the backend that we can use to process the tag.
package validator
import (
"fmt"
"reflect"
"strings"
)
var tagName = "validate"
type Validator interface {
Validate(interface{}) (bool, error)
}
type DefaultValidator struct {
}
func (v DefaultValidator) Validate(val interface{}) (bool, error) {
return true, nil
}
func getValidatorFromTag(tag string) Validator {
args := strings.Split(tag, ",")
switch args[0] {
case "slug":
validator := SlugValidator{}
return validator
case "required":
validator := RequiredValidator{}
nextValidator := strings.Join(args[1:], ",")
if nextValidator != "" {
validator.Next = getValidatorFromTag(nextValidator)
}
return validator
}
return DefaultValidator{}
}
Validation Rule
With the base validation done, now we can just add the validation rule like this. We just need to create struct that implement Validate(interface{})
function.
type SlugValidator struct {
}
func IsDigit(r rune) bool {
return '0' <= r && r <= '9'
}
func IsAlpha(r rune) bool {
return r == '_' || 'a' <= r && r <= 'Z' || 'A' <= r && r <= 'Z'
}
func IsValidSlug(s string) bool {
for _, r := range s {
if IsAlpha(r) || IsDigit(r) || r == '-' {
return true
}
}
return false
}
func (v SlugValidator) Validate(val interface{}) (bool, error) {
if res, ok := val.(string); ok {
if strings.Contains(res, " ") {
return false, fmt.Errorf("Field must not have space")
}
if !lib.IsValidSlug(res) {
return false, fmt.Errorf("Field must be alpha numeric")
}
} else {
return false, fmt.Errorf("Field must be string")
}
return true, nil
}
And here's one more example for required falidation.
type RequiredValidator struct {
Next Validator
}
func (v RequiredValidator) Validate(val interface{}) (bool, error) {
slice := reflect.ValueOf(val)
if slice.Kind() == reflect.Slice {
if slice.Len() == 0 {
return false, fmt.Errorf("field is required.")
}
return true, nil
}
if res, ok := val.(string); ok && res == "" {
return false, fmt.Errorf("field is required.")
}
if res, ok := val.(int64); ok && res == 0 {
return false, fmt.Errorf("field is required.")
}
if v.Next != nil {
return v.Next.Validate(val)
}
return true, nil
}
Conclusion
Thus, if we write our own validation, it will be easier for us to change the rule of the validation in our code. Yes, it will faster to just use the library available on github. However, we will be able to modify the validation rule more quickly if we are able to write it ourselves.