Validating Struct Fields in Go: With and Without Packages

Sachin BorseSachin Borse
4 min read

Validation is essential for ensuring that the data in your application meets specific criteria. Go offers multiple ways to validate struct fields—both with the help of external packages and through custom-built validation logic. In this article, we'll explore both methods, including how to provide custom error messages in validation tags.

Validating Struct Fields with Packages

There are several powerful Go packages for struct validation. They offer built-in validation rules, customizable error messages, and flexibility. One of the most popular options is the go-playground/validator package, which allows you to define validation rules within struct tags.

1. Using go-playground/validator

The go-playground/validator package makes validation straightforward by using struct tags to define rules. It also allows you to include custom error messages through tags, providing user-friendly feedback.

Example Usage:

import (
    "github.com/go-playground/validator/v10"
    "fmt"
)

type User struct {
    Name  string `validate:"required,min=3,max=50" validateMessage:"Name must be between 3 and 50 characters long"`
    Age   int    `validate:"gte=0,lte=130" validateMessage:"Age must be between 0 and 130"`
    Email string `validate:"required,email" validateMessage:"Invalid email format"`
}

func main() {
    validate := validator.New()
    user := User{Name: "Jo", Age: 200, Email: "invalid-email"}

    err := validate.Struct(user)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            field := err.Field()
            customMessage := err.StructField() + " " + err.Tag() // Extract the custom message (simplified)
            fmt.Println(customMessage)
        }
    }
}

In this example, the validateMessage tag would ideally contain the custom error message. However, to actually implement custom error messages, you may need to manually handle these using custom functions.

To truly customize messages in go-playground/validator, you can register custom validation functions:

func main() {
    validate := validator.New()

    // Register a custom validation for the Name field
    validate.RegisterValidation("custom_name", func(fl validator.FieldLevel) bool {
        name := fl.Field().String()
        if len(name) < 3 || len(name) > 50 {
            return false
        }
        return true
    })

    user := User{Name: "Jo", Age: 200, Email: "invalid-email"}
    err := validate.Struct(user)

    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            field := err.Field()
            customMessage := err.Field() + " " + "failed custom validation"
            fmt.Println(customMessage)
        }
    }
}

2. Using ozzo-validation

The ozzo-validation package takes a different approach, allowing you to programmatically define validation rules and custom error messages.

Example Usage:

import (
    "github.com/go-ozzo/ozzo-validation/v4"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email"`
}

func (u User) Validate() error {
    return validation.ValidateStruct(&u,
        validation.Field(&u.Name, validation.Required.Error("Name is required"), validation.Length(3, 50).Error("Name must be between 3 and 50 characters long")),
        validation.Field(&u.Age, validation.Required, validation.Min(0).Error("Age must be greater than 0"), validation.Max(130).Error("Age must be less than or equal to 130")),
        validation.Field(&u.Email, validation.Required.Error("Email is required"), validation.Email.Error("Invalid email format")),
    )
}

func main() {
    user := User{Name: "Jo", Age: 200, Email: "invalid-email"}

    err := user.Validate()
    if err != nil {
        fmt.Println("Validation error:", err)
    }
}

In this example, ozzo-validation allows you to define custom error messages directly in the validation rules, making it easy to provide user-friendly feedback.

Validating Struct Fields Without Packages

If you prefer to avoid external dependencies, you can implement custom validation logic directly in Go. This approach requires you to write your own validation functions and manage error messages manually.

1. Defining the Struct

First, define the struct you want to validate:

type User struct {
    Name  string
    Age   int
    Email string
}

2. Writing Custom Validation Functions

Next, create validation functions for each field, returning custom error messages if the validation fails.

import "errors"

func validateName(name string) error {
    if len(name) < 3 || len(name) > 50 {
        return errors.New("Name must be between 3 and 50 characters long")
    }
    return nil
}

func validateAge(age int) error {
    if age < 0 || age > 130 {
        return errors.New("Age must be between 0 and 130")
    }
    return nil
}

func validateEmail(email string) error {
    if len(email) == 0 || !containsAtSign(email) {
        return errors.New("Invalid email address")
    }
    return nil
}

func containsAtSign(s string) bool {
    for _, ch := range s {
        if ch == '@' {
            return true
        }
    }
    return false
}

3. Combining Validation Functions

Combine these validation functions to validate the entire struct and return error messages if any validation fails.

func validateUser(user User) error {
    if err := validateName(user.Name); err != nil {
        return err
    }
    if err := validateAge(user.Age); err != nil {
        return err
    }
    if err := validateEmail(user.Email); err != nil {
        return err
    }
    return nil
}

4. Using the Validation Function

Finally, use the validation function in your application and handle any validation errors:

func main() {
    user := User{Name: "Jo", Age: 200, Email: "invalid-email"}

    if err := validateUser(user); err != nil {
        fmt.Println("Validation error:", err)
    } else {
        fmt.Println("User is valid!")
    }
}

Conclusion

Whether you choose to use a validation package or write your own validation logic, Go provides several flexible options for ensuring that your data conforms to specific rules. Packages like go-playground/validator and ozzo-validation allow for advanced validation features, including custom error messages. On the other hand, writing custom validation logic gives you full control over how your data is validated. Ultimately, the choice depends on your project’s complexity and your preference for external dependencies.

By understanding both approaches, you can select the method that best suits your application’s needs.

2
Subscribe to my newsletter

Read articles from Sachin Borse directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sachin Borse
Sachin Borse