Skip to main content

Overview

The validation middleware automatically validates incoming requests before they reach your handlers. It supports multiple validation methods including the validator interface, custom validation functions, and protovalidate for Protocol Buffers.

Installation

go get github.com/go-kratos/kratos/v2/middleware/validate

Basic Usage

The Validator function creates a validation middleware:
func Validator(validators ...ValidatorFunc) middleware.Middleware

Server Example

import (
    "github.com/go-kratos/kratos/v2"
    "github.com/go-kratos/kratos/v2/middleware/validate"
    "github.com/go-kratos/kratos/v2/transport/http"
    "github.com/go-kratos/kratos/v2/transport/grpc"
)

func main() {
    // Create HTTP server with validation
    httpSrv := http.NewServer(
        http.Address(":8000"),
        http.Middleware(
            validate.Validator(),
        ),
    )
    
    // Create gRPC server with validation
    grpcSrv := grpc.NewServer(
        grpc.Address(":9000"),
        grpc.Middleware(
            validate.Validator(),
        ),
    )
    
    app := kratos.New(
        kratos.Server(httpSrv, grpcSrv),
    )
    
    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}

Validation Methods

1. Validator Interface

Implement the Validate() method on your request types:
type validator interface {
    Validate() error
}

Usage Example

import (
    "errors"
    "strings"
)

type CreateUserRequest struct {
    Name  string
    Email string
    Age   int32
}

// Implement Validate method
func (r *CreateUserRequest) Validate() error {
    if r.Name == "" {
        return errors.New("name is required")
    }
    if len(r.Name) < 2 {
        return errors.New("name must be at least 2 characters")
    }
    if r.Email == "" {
        return errors.New("email is required")
    }
    if !strings.Contains(r.Email, "@") {
        return errors.New("invalid email format")
    }
    if r.Age < 0 || r.Age > 150 {
        return errors.New("age must be between 0 and 150")
    }
    return nil
}
The middleware automatically calls Validate() on any request that implements the interface.

2. Custom Validator Functions

Pass custom validation functions to the middleware:
type ValidatorFunc func(v any) error

Usage Example

import (
    "github.com/go-kratos/kratos/v2/middleware/validate"
)

// Custom validator function
func customValidator(v any) error {
    // Type assert to your request type
    switch req := v.(type) {
    case *CreateUserRequest:
        if req.Name == "admin" {
            return errors.New("cannot use reserved name")
        }
    }
    return nil
}

// Use multiple validators
validate.Validator(
    customValidator,
    anotherValidator,
)

3. Protocol Buffers Validation

For Protocol Buffers, use protovalidate or protoc-gen-validate:

Using buf protovalidate

go get buf.build/go/protovalidate
go get github.com/go-kratos/contrib/middleware/validate
import (
    validate "github.com/go-kratos/contrib/middleware/validate"
    "github.com/go-kratos/kratos/v2/transport/http"
)

// Use protovalidate middleware
httpSrv := http.NewServer(
    http.Address(":8000"),
    http.Middleware(
        validate.ProtoValidate(),
    ),
)
Proto definition with validation rules:
syntax = "proto3";

import "buf/validate/validate.proto";

message CreateUserRequest {
  string name = 1 [(buf.validate.field).string = {
    min_len: 2,
    max_len: 50
  }];
  
  string email = 2 [(buf.validate.field).string.email = true];
  
  int32 age = 3 [(buf.validate.field).int32 = {
    gte: 0,
    lte: 150
  }];
}

Using Google AIP Field Behavior

import (
    "google.golang.org/protobuf/proto"
    "go.einride.tech/aip/fieldbehavior"
    "github.com/go-kratos/kratos/v2/middleware/validate"
)

// Validator for required fields
func aipValidator(v any) error {
    if msg, ok := v.(proto.Message); ok {
        if err := fieldbehavior.ValidateRequiredFields(msg); err != nil {
            return err
        }
    }
    return nil
}

validate.Validator(aipValidator)
Proto definition:
syntax = "proto3";

import "google/api/field_behavior.proto";

message CreateUserRequest {
  string name = 1 [(google.api.field_behavior) = REQUIRED];
  string email = 2 [(google.api.field_behavior) = REQUIRED];
  int32 age = 3 [(google.api.field_behavior) = OPTIONAL];
}

Error Handling

Validation errors are returned as:
errors.BadRequest("VALIDATOR", err.Error()).WithCause(err)
This returns:
  • HTTP status: 400 Bad Request
  • gRPC code: INVALID_ARGUMENT
  • Reason: "VALIDATOR"

Error Response Example

{
  "code": 400,
  "reason": "VALIDATOR",
  "message": "name is required",
  "metadata": {}
}

Validation Order

The middleware validates in this order:
  1. Built-in Validate() method (if request implements validator interface)
  2. Custom validator functions (in the order provided)
If any validation fails, subsequent validators are not executed.

Complete Example

package main

import (
    "context"
    "errors"
    "log"
    "strings"
    
    "github.com/go-kratos/kratos/v2"
    kratosErrors "github.com/go-kratos/kratos/v2/errors"
    "github.com/go-kratos/kratos/v2/middleware/validate"
    "github.com/go-kratos/kratos/v2/transport/http"
)

// Request type with validation
type CreateUserRequest struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    Age      int32  `json:"age"`
    Role     string `json:"role"`
}

// Implement validator interface
func (r *CreateUserRequest) Validate() error {
    if r.Name == "" {
        return errors.New("name is required")
    }
    if len(r.Name) < 2 {
        return errors.New("name must be at least 2 characters")
    }
    if r.Email == "" {
        return errors.New("email is required")
    }
    if !strings.Contains(r.Email, "@") {
        return errors.New("invalid email format")
    }
    if r.Age < 0 || r.Age > 150 {
        return errors.New("age must be between 0 and 150")
    }
    return nil
}

// Custom business logic validator
func businessRuleValidator(v any) error {
    switch req := v.(type) {
    case *CreateUserRequest:
        // Check reserved names
        reservedNames := []string{"admin", "root", "system"}
        for _, reserved := range reservedNames {
            if strings.EqualFold(req.Name, reserved) {
                return errors.New("name is reserved and cannot be used")
            }
        }
        
        // Validate role
        validRoles := []string{"user", "moderator", "admin"}
        roleValid := false
        for _, role := range validRoles {
            if req.Role == role {
                roleValid = true
                break
            }
        }
        if !roleValid {
            return errors.New("invalid role")
        }
    }
    return nil
}

// Service implementation
type UserService struct{}

func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) error {
    // Validation is already done by middleware
    log.Printf("Creating user: %s (%s)\n", req.Name, req.Email)
    return nil
}

func main() {
    // Create HTTP server with validation
    httpSrv := http.NewServer(
        http.Address(":8000"),
        http.Middleware(
            validate.Validator(
                businessRuleValidator,
            ),
        ),
    )
    
    app := kratos.New(
        kratos.Name("validation-example"),
        kratos.Server(httpSrv),
    )
    
    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}

Testing Validation

package main

import (
    "context"
    "testing"
    
    "github.com/go-kratos/kratos/v2/middleware/validate"
)

func TestRequestValidation(t *testing.T) {
    tests := []struct {
        name    string
        req     *CreateUserRequest
        wantErr bool
    }{
        {
            name: "valid request",
            req: &CreateUserRequest{
                Name:  "John Doe",
                Email: "[email protected]",
                Age:   30,
            },
            wantErr: false,
        },
        {
            name: "missing name",
            req: &CreateUserRequest{
                Email: "[email protected]",
                Age:   30,
            },
            wantErr: true,
        },
        {
            name: "invalid email",
            req: &CreateUserRequest{
                Name:  "John Doe",
                Email: "invalid-email",
                Age:   30,
            },
            wantErr: true,
        },
        {
            name: "invalid age",
            req: &CreateUserRequest{
                Name:  "John Doe",
                Email: "[email protected]",
                Age:   200,
            },
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.req.Validate()
            if (err != nil) != tt.wantErr {
                t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

Advanced Validation

Conditional Validation

func (r *CreateUserRequest) Validate() error {
    // Basic validation
    if r.Name == "" {
        return errors.New("name is required")
    }
    
    // Conditional validation
    if r.Role == "admin" {
        if r.Age < 18 {
            return errors.New("admin users must be 18 or older")
        }
    }
    
    return nil
}

Cross-Field Validation

type DateRange struct {
    StartDate time.Time
    EndDate   time.Time
}

func (r *DateRange) Validate() error {
    if r.StartDate.IsZero() {
        return errors.New("start_date is required")
    }
    if r.EndDate.IsZero() {
        return errors.New("end_date is required")
    }
    if r.EndDate.Before(r.StartDate) {
        return errors.New("end_date must be after start_date")
    }
    return nil
}

Using Third-Party Validators

go-playground/validator

go get github.com/go-playground/validator/v10
import (
    "github.com/go-playground/validator/v10"
    "github.com/go-kratos/kratos/v2/middleware/validate"
)

var v = validator.New()

// Validator function
func goPlaygroundValidator(req any) error {
    return v.Struct(req)
}

// Request with tags
type CreateUserRequest struct {
    Name  string `json:"name" validate:"required,min=2,max=50"`
    Email string `json:"email" validate:"required,email"`
    Age   int32  `json:"age" validate:"gte=0,lte=150"`
}

// Use in middleware
validate.Validator(goPlaygroundValidator)

Best Practices

Always place validation middleware early in the chain, after recovery but before business logic.
http.Middleware(
    recovery.Recovery(),
    validate.Validator(),
    logging.Server(logger),
    // ... business middleware
)
Validation should check format and constraints, not business logic. Use separate middleware for business rules.
// Good: Format validation
if !emailRegex.Match(email) {
    return errors.New("invalid email format")
}

// Bad: Business logic in validation
if userExists(email) {
    return errors.New("user already exists")
}
Provide specific, actionable error messages that help users fix their requests.
// Good
return errors.New("age must be between 0 and 150")

// Bad
return errors.New("invalid age")
For Protocol Buffers, use protovalidate to define validation rules in .proto files.
Write comprehensive tests for all validation rules including edge cases.
Implement validation once in the middleware, not in every handler.

Selective Validation

Apply validation to specific routes:
import (
    "github.com/go-kratos/kratos/v2/middleware/validate"
    "github.com/go-kratos/kratos/v2/middleware/selector"
)

// Apply validation only to write operations
http.Middleware(
    selector.Server(
        validate.Validator(),
    ).Match(func(ctx context.Context, operation string) bool {
        // Validate POST, PUT, PATCH
        return strings.Contains(operation, "Create") ||
               strings.Contains(operation, "Update") ||
               strings.Contains(operation, "Delete")
    }).Build(),
)

Common Validation Rules

Here are common validation patterns:
func (r *Request) Validate() error {
    // Required field
    if r.Field == "" {
        return errors.New("field is required")
    }
    
    // Length constraints
    if len(r.Field) < 2 || len(r.Field) > 50 {
        return errors.New("field must be 2-50 characters")
    }
    
    // Numeric range
    if r.Number < 0 || r.Number > 100 {
        return errors.New("number must be 0-100")
    }
    
    // Regex pattern
    if !regexp.MustCompile(`^[a-zA-Z]+$`).MatchString(r.Field) {
        return errors.New("field must contain only letters")
    }
    
    // Email format
    if !strings.Contains(r.Email, "@") || !strings.Contains(r.Email, ".") {
        return errors.New("invalid email format")
    }
    
    // URL format
    if _, err := url.Parse(r.URL); err != nil {
        return errors.New("invalid URL format")
    }
    
    // Enum validation
    validValues := map[string]bool{"option1": true, "option2": true}
    if !validValues[r.Field] {
        return errors.New("invalid value")
    }
    
    return nil
}

Source Reference

The validation middleware implementation can be found in:
  • middleware/validate/validate.go:44 - Validator middleware
  • middleware/validate/validate.go:10 - ValidatorFunc type
  • middleware/validate/validate.go:13 - validator interface
  • contrib/middleware/validate/validate.go:18 - ProtoValidate middleware

Next Steps

Authentication

Add JWT authentication

Logging

Log validation failures

Build docs developers (and LLMs) love