Validation
Introduction
Good validation prevents bugs and enhances the security of your application. In this guide, we demonstrate how to validate incoming data in a Go application without using external libraries.
Writing the Validation Logic
In this example, we validate the CreateUser
request. The ValidateCreateUser
function will handle the validation logic separately from the controller.
package user
import (
"encoding/json"
"errors"
"net/http"
"regexp"
"strings"
"github.com/confetti-framework/handler"
)
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
func CreateUser(response http.ResponseWriter, request *http.Request) error {
var user User
decoder := json.NewDecoder(request.Body)
if err := decoder.Decode(&user); err != nil {
return handler.NewSystemError(err, "ag84r3g")
}
if err := ValidateCreateUser(user); err != nil {
return err
}
response.WriteHeader(http.StatusCreated)
response.Write([]byte("User created successfully"))
return nil
}
func ValidateCreateUser(user User) error {
if strings.TrimSpace(user.Name) == "" {
return handler.NewUserError("Name is required", http.StatusUnprocessableEntity)
}
if !isValidEmail(user.Email) {
return handler.NewUserError("Invalid email format", http.StatusUnprocessableEntity)
}
return nil
}
func isValidEmail(email string) bool {
emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
re := regexp.MustCompile(emailRegex)
return re.MatchString(email)
}
Handling Nested Attributes
For nested structures, we extend validation by checking nested fields.
type Author struct {
Name string `json:"name"`
Description string `json:"description"`
}
type Post struct {
Title string `json:"title"`
Author Author `json:"author"`
}
func ValidatePost(post Post) error {
if strings.TrimSpace(post.Title) == "" {
return handler.NewUserError("Title is required", http.StatusUnprocessableEntity)
}
if strings.TrimSpace(post.Author.Name) == "" {
return handler.NewUserError("Author name is required", http.StatusUnprocessableEntity)
}
if strings.TrimSpace(post.Author.Description) == "" {
return handler.NewUserError("Author description is required", http.StatusUnprocessableEntity)
}
return nil
}
Validating Lists
To validate lists of objects, we loop through each item.
type Order struct {
Street string `json:"street"`
}
type RequestData struct {
Orders []Order `json:"orders"`
}
func ValidateRequestData(data RequestData) error {
if len(data.Orders) != 3 {
return handler.NewUserError("Orders must contain exactly 3 items", http.StatusUnprocessableEntity)
}
for i, order := range data.Orders {
if strings.TrimSpace(order.Street) == "" {
return handler.NewUserError("Street in order "+string(i)+" is required", http.StatusUnprocessableEntity)
}
}
return nil
}
Custom Validation Rules
Using structs for validation improves reusability, maintainability, and testability. By encapsulating validation logic within a struct, you can reuse the same rule across multiple fields and controllers. This keeps the code clean and avoids duplication.
type UppercaseRule struct{}
func (u UppercaseRule) Validate(value string) error {
if value != strings.ToUpper(value) {
return handler.NewUserError("The value must be uppercase", http.StatusUnprocessableEntity)
}
return nil
}
Usage:
uppercaseRule := UppercaseRule{}
if err := uppercaseRule.Validate(user.Name); err != nil {
return err
}
Examples
Required
Description:
Validates that the input value is not empty or composed solely of whitespace.
package rule
import (
"net/http"
"strings"
)
type Required struct{}
func (r Required) Validate(value string) error {
if strings.TrimSpace(value) == "" {
return handler.NewUserError("This field is required", http.StatusUnprocessableEntity)
}
return nil
}
Description:
Checks whether the input value is in a valid email address format.
package rule
import (
"net/http"
"regexp"
)
type Email struct{}
func (e Email) Validate(value string) error {
re := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
if !re.MatchString(value) {
return handler.NewUserError("The value must be a valid email address", http.StatusUnprocessableEntity)
}
return nil
}
URL
Description:
Ensures the input value is a valid URL by parsing it.
package rule
import (
"net/http"
"net/url"
)
type URL struct{}
func (u URL) Validate(value string) error {
if _, err := url.ParseRequestURI(value); err != nil {
return handler.NewUserError("The value must be a valid URL", http.StatusUnprocessableEntity)
}
return nil
}
Alpha
Description:
Validates that the input contains only alphabetic characters (A–Z and a–z).
package rule
import (
"net/http"
"regexp"
)
type Alpha struct{}
func (a Alpha) Validate(value string) error {
re := regexp.MustCompile(`^[A-Za-z]+$`)
if !re.MatchString(value) {
return handler.NewUserError("The value must contain only alphabetic characters", http.StatusUnprocessableEntity)
}
return nil
}
AlphaNumeric
Description:
Checks that the input consists solely of alphabetic and numeric characters.
package rule
import (
"net/http"
"regexp"
)
type AlphaNumeric struct{}
func (an AlphaNumeric) Validate(value string) error {
re := regexp.MustCompile(`^[A-Za-z0-9]+$`)
if !re.MatchString(value) {
return handler.NewUserError("The value must be alphanumeric", http.StatusUnprocessableEntity)
}
return nil
}
Numeric
Description:
Verifies that the input can be parsed as a numeric value (supports integers and floats).
package rule
import (
"net/http"
"strconv"
)
type Numeric struct{}
func (n Numeric) Validate(value string) error {
if _, err := strconv.ParseFloat(value, 64); err != nil {
return handler.NewUserError("The value must be numeric", http.StatusUnprocessableEntity)
}
return nil
}
MinLength
Description:
Ensures that the input value has at least a specified minimum number of characters.
package rule
import (
"net/http"
"strconv"
"unicode/utf8"
)
type MinLength struct {
Min int
}
func (m MinLength) Validate(value string) error {
if utf8.RuneCountInString(value) < m.Min {
return handler.NewUserError("The value must be at least "+strconv.Itoa(m.Min)+" characters long", http.StatusUnprocessableEntity)
}
return nil
}
MaxLength
Description:
Checks that the input value does not exceed a specified maximum number of characters.
package rule
import (
"net/http"
"strconv"
"unicode/utf8"
)
type MaxLength struct {
Max int
}
func (m MaxLength) Validate(value string) error {
if utf8.RuneCountInString(value) > m.Max {
return handler.NewUserError("The value must be at most "+strconv.Itoa(m.Max)+" characters long", http.StatusUnprocessableEntity)
}
return nil
}
BetweenLength
Description:
Validates that the input’s length falls between a defined minimum and maximum range.
package rule
import (
"net/http"
"strconv"
"unicode/utf8"
)
type BetweenLength struct {
Min int
Max int
}
func (b BetweenLength) Validate(value string) error {
length := utf8.RuneCountInString(value)
if length < b.Min || length > b.Max {
return handler.NewUserError("The value must be between "+strconv.Itoa(b.Min)+" and "+strconv.Itoa(b.Max)+" characters long", http.StatusUnprocessableEntity)
}
return nil
}
In
Description:
Checks if the input value is one of a predefined list of allowed options.
package rule
import (
"net/http"
)
type In struct {
Options []string
}
func (i In) Validate(value string) error {
for _, option := range i.Options {
if value == option {
return nil
}
}
return handler.NewUserError("The value must be one of the allowed options", http.StatusUnprocessableEntity)
}
NotIn
Description:
Ensures that the input value is not present in a given list of disallowed options.
package rule
import (
"net/http"
)
type NotIn struct {
Options []string
}
func (n NotIn) Validate(value string) error {
for _, option := range n.Options {
if value == option {
return handler.NewUserError("The value is not allowed", http.StatusUnprocessableEntity)
}
}
return nil
}
Regex
Description:
Validates that the input matches a specified regular expression pattern.
package rule
import (
"net/http"
"regexp"
)
type Regex struct {
Pattern string
}
func (r Regex) Validate(value string) error {
re := regexp.MustCompile(r.Pattern)
if !re.MatchString(value) {
return handler.NewUserError("The value does not match the required pattern", http.StatusUnprocessableEntity)
}
return nil
}
Date
Description:
Checks if the input value is a valid date based on the provided format.
package rule
import (
"net/http"
"time"
)
type Date struct {
Format string
}
func (d Date) Validate(value string) error {
if _, err := time.Parse(d.Format, value); err != nil {
return handler.NewUserError("The value must be a valid date", http.StatusUnprocessableEntity)
}
return nil
}
AfterDate
Description:
Ensures the input date is later than a specified date.
package rule
import (
"net/http"
"time"
)
type AfterDate struct {
Date time.Time
Format string
}
func (a AfterDate) Validate(value string) error {
parsed, err := time.Parse(a.Format, value)
if err != nil {
return handler.NewUserError("The value must be a valid date", http.StatusUnprocessableEntity)
}
if !parsed.After(a.Date) {
return handler.NewUserError("The date must be after "+a.Date.Format(a.Format), http.StatusUnprocessableEntity)
}
return nil
}
BeforeDate
Description:
Validates that the input date occurs before a specified date.
package rule
import (
"net/http"
"time"
)
type BeforeDate struct {
Date time.Time
Format string
}
func (b BeforeDate) Validate(value string) error {
parsed, err := time.Parse(b.Format, value)
if err != nil {
return handler.NewUserError("The value must be a valid date", http.StatusUnprocessableEntity)
}
if !parsed.Before(b.Date) {
return handler.NewUserError("The date must be before "+b.Date.Format(b.Format), http.StatusUnprocessableEntity)
}
return nil
}
Integer
Description:
Checks that the input value can be parsed as an integer.
package rule
import (
"net/http"
"strconv"
)
type Integer struct{}
func (i Integer) Validate(value string) error {
if _, err := strconv.Atoi(value); err != nil {
return handler.NewUserError("The value must be an integer", http.StatusUnprocessableEntity)
}
return nil
}
Boolean
Description:
Validates that the input is either "true"
or "false"
, ignoring case.
package rule
import (
"net/http"
"strings"
)
type Boolean struct{}
func (b Boolean) Validate(value string) error {
lower := strings.ToLower(value)
if lower != "true" && lower != "false" {
return handler.NewUserError("The value must be a boolean", http.StatusUnprocessableEntity)
}
return nil
}
UUID
Description:
Checks that the input value matches the UUID format (version 1–5).
package rule
import (
"net/http"
"regexp"
)
type UUID struct{}
func (u UUID) Validate(value string) error {
re := regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$`)
if !re.MatchString(value) {
return handler.NewUserError("The value must be a valid UUID", http.StatusUnprocessableEntity)
}
return nil
}
Lowercase
Description:
Ensures that the input value is entirely in lowercase letters.
package rule
import (
"net/http"
"strings"
)
type Lowercase struct{}
func (l Lowercase) Validate(value string) error {
if value != strings.ToLower(value) {
return handler.NewUserError("The value must be lowercase", http.StatusUnprocessableEntity)
}
return nil
}