DEV Community

Cover image for Request Validation with Go
Gayan Hewa
Gayan Hewa

Posted on

7

Request Validation with Go

#go

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.

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay