DEV Community

Huynh Thanh Phuc
Huynh Thanh Phuc

Posted on • Updated on

Customizing Error Messages in Struct Validation Using Tags in Go

Introduction

Struct validation is an essential aspect of building robust applications in Go. It ensures that the data we receive from the process adheres to the expected format and constraints. While the standard validation package in Go provides useful features, customize error messages in struct validation using tags, along with a practical example.

Overview of Struct Validation in Go

Package: https://pkg.go.dev/github.com/go-playground/validator/v10

Struct validation is a crucial process in software development. It ensures that the data received during the process adheres to the expected format and constraints, preventing errors and ensuring data integrity. In Go, this process is made easy by the standard validation package, which provides a variety of useful features.

The validation package in Go allows developers to specify constraints for struct fields using tags. These constraints can include maximum and minimum values, regular expressions, and custom validation functions. By adding these tags to the struct fields, developers can ensure that the data being received meets the expected format and constraints.

Another benefit of using the validation package in Go is the ability to customize error messages. Developers can add tags to their struct fields that specify custom error messages for each constraint. This can help to make error messages more informative and user-friendly, improving the overall user experience.

To demonstrate the power of struct validation in Go, let's consider a practical example. Suppose we are building an e-commerce platform that requires users to enter their payment information. Using the validation package, we can ensure that the credit card number entered by the user is valid and meets the expected format. We can also ensure that the expiration date is valid and that the CVV code is three digits long.

By using the validation package in Go, we can ensure that our e-commerce platform is secure and error-free. This will improve the user experience and help to build trust with our customers.

Section 2: Standard Struct Validation in Go

The validation package in Go provides a variety of useful features for struct validation. To use the validation package, we first need to create a validator instance. We can create a new validator instance using the New() function provided by the validation package.

import "github.com/go-playground/validator/v10"

// Create a new validator instance
validate := validator.New()

Enter fullscreen mode Exit fullscreen mode

Once we have a validator instance, we can use it to validate our struct fields. To do this, we add tags to our struct fields that specify the desired constraints.

type PaymentInfo struct {
    CreditCardNumber string `validate:"required"`
    CVV              string `validate:"required,len=3"`
}

Enter fullscreen mode Exit fullscreen mode

In this example, we have added tags to the fields of our PaymentInfo struct. These tags specify that the CreditCardNumber field must required, the CVV field must contain exactly three digits.

We can then use the validator instance to validate our struct fields.

// Create a new PaymentInfo instance
paymentInfo := PaymentInfo{
    CreditCardNumber: "4111111111111111",
    CVV:              "123",
}

// Validate the PaymentInfo instance
err := validate.Struct(paymentInfo)
if err != nil {
    // Handle validation error
}

Enter fullscreen mode Exit fullscreen mode

If any of the struct fields fail validation, the validate.Struct() function will return a validation error. We can then handle this error in our application code.

Key: 'PaymentInfo.CreditCardNumber' Error:Field validation for 'CreditCardNumber' failed on the 'required' tag
Key: 'PaymentInfo.CVV' Error:Field validation for 'CVV' failed on the 'len' tag

By using the validation package in Go, we can ensure that our struct fields meet the expected format and constraints, improving the overall quality and security of our applications.

Section 3: Customizing Error Messages with Tags

In addition to specifying constraints for struct fields, we can also customize error messages using tags. To do this, we add a message tag to our struct field tags.

type PaymentInfo struct {
    CreditCardNumber string `validate:"required" errormgs:"Invalid credit card is required xxx"`
    CVV              string `validate:"required,len=3" errormgs:"CVV code must be three digits long"`
}

func (m *PaymentInfo) Validate(validate *validator.Validate) error {
    return ValidateFunc[PaymentInfo](*m, validate)
}
Enter fullscreen mode Exit fullscreen mode

My ValidateFunc:

const tagCustom = "errormgs"

func errorTagFunc[T interface{}](obj interface{}, snp string, fieldname, actualTag string) error {
    o := obj.(T)

    if !strings.Contains(snp, fieldname) {
        return nil
    }

    fieldArr := strings.Split(snp, ".")
    rsf := reflect.TypeOf(o)

    for i := 1; i < len(fieldArr); i++ {
        field, found := rsf.FieldByName(fieldArr[i])
        if found {
            if fieldArr[i] == fieldname {
                customMessage := field.Tag.Get(tagCustom)
                if customMessage != "" {
                    return fmt.Errorf("%s: %s (%s)", fieldname, customMessage, actualTag)
                }
                return nil
            } else {
                if field.Type.Kind() == reflect.Ptr {
                    // If the field type is a pointer, dereference it
                    rsf = field.Type.Elem()
                } else {
                    rsf = field.Type
                }
            }
        }
    }
    return nil
}

func ValidateFunc[T interface{}](obj interface{}, validate *validator.Validate) (errs error) {
    o := obj.(T)

    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in Validate:", r)
            errs = fmt.Errorf("can't validate %+v", r)
        }
    }()

    if err := validate.Struct(o); err != nil {
        errorValid := err.(validator.ValidationErrors)
        for _, e := range errorValid {
            // snp  X.Y.Z
            snp := e.StructNamespace()
            errmgs := errorTagFunc[T](obj, snp, e.Field(), e.ActualTag())
            if errmgs != nil {
                errs = errors.Join(errs, fmt.Errorf("%w", errmgs))
            } else {
                errs = errors.Join(errs, fmt.Errorf("%w", e))
            }
        }
    }

    if errs != nil {
        return errs
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Section 4: Practical Example

// Create a new PaymentInfo instance
paymentInfo := PaymentInfo{
    CreditCardNumber: "",
    CVV:              "123",
}

// Validate the PaymentInfo instance
err := paymentInfo.Validate(validate)
Enter fullscreen mode Exit fullscreen mode

The error messages we specified in the errormgs tag will be returned if the field fails validation. For example, if we set the CreditCardNumber to an empty string and validate the PaymentInfo instance, the error message "Invalid credit card is required xxx" will be returned. By customizing error messages in this way, we can make them more informative and user-friendly, improving the overall user experience of our applications.

CreditCardNumber: Invalid credit card is required xxx (required)
CVV: CVV code must be three digits long (len)
Enter fullscreen mode Exit fullscreen mode

https://go.dev/play/p/Tigv2u0o_51

Section 6: Conclusion

In conclusion, struct validation is an essential aspect of building robust applications in Go. While the standard validation package provides a variety of useful features, customizing error messages with tags can make them more informative and user-friendly, enhancing the overall user experience. By following the practical example presented in this article, developers can ensure that their struct fields meet the expected format and constraints, building trust with their users and improving the quality of their applications.

Buy Me a Coffee:
Buy Me A Coffee

Top comments (7)

Collapse
 
atiwari44 profile image
at

Hi, thanks for the wonderful post. BTW, it does not seem to work when you have a field which is a pointer to struct. Do I miss something? In below example, it panics (error: can't validate reflect: FieldByName of non-struct type *...)when Text is nil.

For example:
type Request struct{
Message *Message validate:"required"
}
type Message struct{
Text Text validate:"required"
}
type Text {}

Collapse
 
thanhphuchuynh profile image
Huynh Thanh Phuc

Hi, I try with a pointer to struct, it still work. hmmm
You can check it at go.dev/play/p/FeL8HYFyFs0

Collapse
 
atiwari44 profile image
at • Edited

I see that it throws following error (as it panics)
Recovered in Validate: reflect: FieldByName of non-struct type *main.Message
can't validate reflect: FieldByName of non-struct type *main.Message

It fails when processing validation error for the Text field (required validation).

I solved this by checking rsf Type before trying rsf.FieldByName(…). If the type is pointer then I use rsf.Elem().FieldByName().

Thread Thread
 
thanhphuchuynh profile image
Huynh Thanh Phuc

Hi, sorry my mistake. I miss case check pointer. I have change code this post. help me check, thanks :3
go.dev/play/p/Tigv2u0o_51

Collapse
 
lucasoares profile image
Lucas Soares

Does it works with maps and slices pointing to structs and using 'dive' validator?

It seems it keeps the default error message on these cases...

Collapse
 
thanhphuchuynh profile image
Huynh Thanh Phuc

thank you for your comment! it doesn't work with 'dive' validator

Collapse
 
thdan profile image
Dant

This post's very useful. Tks bro