Go Validator

Nested Validation

The go-validator package supports nested validation , allowing you to define validation rules for fields within nested objects. This is particularly useful when working with complex JSON structures, such as user profiles with address details or order forms with product information.

How Nested Validation Works

  1. Define Nested Validation Rules

    • Use the Nested field in a ValidationOption to specify validation rules for nested objects.
    • Each nested object can have its own set of ValidationOption rules.
  2. Error Messages

    • When a validation fails, the error message returned is exactly the one specified by the developer in the Message field of the Validator.
    • The package does not modify the error message to include the full path to the field. This ensures that the error messages remain simple and user-defined.
  3. Recursive Validation

    • The Validate function automatically handles nested objects by recursively applying the validation rules.

Example: Validating a Nested Object

Below is an example of how to validate a nested object using the go-validator package.

Scenario

You want to validate a user object that contains an optional address field. The address field itself has two required fields: city and zipcode.

Code Example

package main
 
import (
    "errors"
    "fmt"
    "net/http"
 
    "github.com/gin-gonic/gin"
    "github.com/kthehatter/go-validator/validator"
    "github.com/kthehatter/go-validator/validator/ginadapter"
)
 
func main() {
    // Create a new Gin router
    r := gin.New()
 
    // Define validation rules
    validationOptions := []validator.ValidationOption{
        {
            Key:        "username",
            IsOptional: false,
            Validators: []validator.Validator{
                validator.CreateValidator(validator.IsNotEmpty, "Username is required"),
            },
        },
        {
            Key:        "email",
            IsOptional: false,
            Validators: []validator.Validator{
                validator.CreateValidator(validator.IsEmail, "Invalid email address"),
            },
        },
        {
            Key:        "address", // Nested object
            IsOptional: true,      // Address is optional
            Nested: []validator.ValidationOption{
                {
                    Key:        "city",
                    IsOptional: false,
                    Validators: []validator.Validator{
                        validator.CreateValidator(validator.IsNotEmpty, "City is required"),
                    },
                },
                {
                    Key:        "zipcode",
                    IsOptional: false,
                    Validators: []validator.Validator{
                        validator.CreateValidator(validator.IsNumeric, "Zipcode must be numeric"),
                    },
                },
            },
        },
    }
 
    // Apply the validation middleware to the POST /user endpoint
    r.POST("/user", ginadapter.Middleware(validationOptions), func(c *gin.Context) {
        // Retrieve the validated body from the context
        body := c.MustGet("validatedBody").(map[string]interface{})
 
        // Process the data (for demonstration, just return it)
        c.JSON(http.StatusOK, gin.H{
            "message": "User created successfully",
            "data":    body,
        })
    })
 
    // Start the Gin server
    r.Run(":8080")
}

Testing the Application

Step 1: Run the Application

Run the application using:

go run main.go

Step 2: Test with Valid Data

Send a valid POST request to the /user endpoint:

curl -X POST http://localhost:8080/user \
-H "Content-Type: application/json" \
-d '{
    "username": "johndoe",
    "email": "john.doe@example.com",
    "address": {
        "city": "New York",
        "zipcode": "10001"
    }
}'

Expected response:

{
    "message": "User created successfully",
    "data": {
        "username": "johndoe",
        "email": "john.doe@example.com",
        "address": {
            "city": "New York",
            "zipcode": "10001"
        }
    }
}

Step 3: Test with Invalid Data

Send an invalid POST request to the /user endpoint:

curl -X POST http://localhost:8080/user \
-H "Content-Type: application/json" \
-d '{
    "username": "johndoe",
    "email": "invalid-email",
    "address": {
        "city": "",
        "zipcode": "abc"
    }
}'

Expected response:

{
    "message": "City is required"
}

Key Takeaways

  1. Simple Error Messages

    • The error message returned is exactly the one specified by the developer in the Message field of the Validator.
    • The package does not modify the error message to include the full path to the field.
  2. First Error Only

    • If multiple fields fail validation, the package returns the first error encountered. This ensures that the error response remains concise.
  3. Optional Nested Objects

    • Mark nested objects as optional using the IsOptional field.
  4. Reusability

    • Nested validation rules are reusable and can be applied to multiple fields.

Advanced Example: Deeply Nested Objects

You can validate deeply nested objects by chaining multiple levels of Nested fields.

Scenario

You want to validate a user object that contains an address field, which itself contains a coordinates field with latitude and longitude.

Code Example

validationOptions := []validator.ValidationOption{
    {
        Key:        "address",
        IsOptional: true,
        Nested: []validator.ValidationOption{
            {
                Key:        "city",
                IsOptional: false,
                Validators: []validator.Validator{
                    validator.CreateValidator(validator.IsNotEmpty, "City is required"),
                },
            },
            {
                Key:        "coordinates",
                IsOptional: false,
                Nested: []validator.ValidationOption{
                    {
                        Key:        "latitude",
                        IsOptional: false,
                        Validators: []validator.Validator{
                            validator.CreateValidator(validator.IsNumeric, "Latitude must be numeric"),
                        },
                    },
                    {
                        Key:        "longitude",
                        IsOptional: false,
                        Validators: []validator.Validator{
                            validator.CreateValidator(validator.IsNumeric, "Longitude must be numeric"),
                        },
                    },
                },
            },
        },
    },
}

On this page