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)