Request Validation with Go
Tags: go
Published: 2020-04-18 04:49:49

Building APIs with Go is a breeze. The tooling just makes the experience a feast. One of the tricky situations I came across was when I had to add validations for my requests. By default, a Go struct gets validated for the default types defined.

```
  type Customer struct {
    ID uuid.UUID `json:"id"`
    Ref string `json:"ref"`
    FirstName string `json:"first_name"`
    LastName string `json:"last_name,omitempty"
  }
```  

After running through a bunch of validation frameworks available, I decided to go ahead with https://github.com/go-playground/validator

The framework provided is pretty basic, foundational and the API is pretty powerful. I had a scenario where a Customer can be created with the reference only. And the rest of the fields were optional. I opted to use struct level validations to simplify things. So my struct looked something like this:


```
  type Customer struct {
    ID uuid.UUID `json:"id"`
    Ref string `json:"ref" validate:"required,alphanumeric,min=3"`
    FirstName string `json:"first_name" validate:"omitempty,required"`
    LastName string `json:"last_name,omitempty" validate:"omitempty,required"`
  }
```  

Basically, this translates into:

Make the reference required, but the rest of the fields optional and validated only if the value is a non-zero value. 

The tricky bit was the update requests. Whereas the PATCH request would have a partial set of data that would update the Customer struct. 

```
  type Customer struct {
    ID uuid.UUID `json:"id"`
    Ref string `json:"ref" validate:"omitempty,alphanumeric,min=3"`
    FirstName string `json:"first_name" validate:"omitempty,required"`
    LastName string `json:"last_name,omitempty"  validate:"omitempty,required"`
  }
```  

This translates into omitting any record that is not present and only set the stuff that is not a default value.

Because of this situation, I had to figure out a way to balance this out. One option was to have different structs to map into Create/Update requests or to have separate validation rules in the same struct. 

I opted for the 2nd option. Simply because the validator library allowed us to have custom validate tags. With this, my struct looked something like the following:  

```
  type Customer struct {
    ID uuid.UUID `json:"id"`
    Ref string `json:"ref" validate:"required,alphanumeric,min=3" updatereq:"omitempty,alphanumeric,min=3"`
    FirstName string `json:"first_name" validate:"omitempty,required" updatereq:"omitempty,required"`
    LastName string `json:"last_name,omitempty" validate:"omitempty,required" updatereq:"omitempty,required"`
  }
```   

This works, in order for this to work when I validate, I modified my validation function to pick the correct tag. The line `	v.SetTagName("updatereq")` is what we are looking at.

```
// ValidateUpdateReq the given struct.
func ValidateUpdateReq(i interface{}) (bool, map[string]string) {
	errors := make(map[string]string)
	v := validator.New()
	v.SetTagName("updatereq")
	if err := v.Struct(i); err != nil {
		for _, err := range err.(validator.ValidationErrors) {
			if err.Tag() == "email" {
				errors[strings.ToLower(err.Field())] = "Invalid E-mail format."
				continue
			}
			errors[strings.ToLower(err.Field())] = fmt.Sprintf("%s is %s %s", err.Field(), err.Tag(), err.Param())
		}
		return false, errors
	}
	return true, nil
}
```

Yes, I am duplicating the validation function. Purely because it's too early to make any abstraction in the API that I am working on.

The struct looks a bit bloated with redundant validation rules that can be shared, yes. This can be dealt with when the time comes. By replacing the `SetTagName` to use `RegisterTagNameFunc` and have a logic that would resolve tag names dynamically. But, for my case its a bit too early to go for that form of abstraction. 

This approach helps me solve the problem I was facing by using a single data structure and decoupling the validation based on the type into tags.

I would love to know if you guys have any other recommended approaches. Or interesting way's this was solved.