PHANTOM DEV

PHANTOM DEV

Request Validation with Go

Request Validation with Go

Subscribe to my newsletter and never miss my upcoming articles

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 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.

 
Share this