Skip to content

Support validation markers for slice/array elements #80

Description

@sivchari

Support validation markers for slice/array elements

Overview

Currently, validation markers like gt, lt, required, email, maxlength, etc. can only be applied to single fields. This proposal suggests extending these markers to also validate individual elements within slices and arrays.

Current Limitations

Currently supported:

type User struct {
    // +govalid:gt=18
    Age int `json:"age"`  // ✅ Single numeric field
    
    // +govalid:email
    Email string `json:"email"`  // ✅ Single string field
}

Currently NOT supported:

type User struct {
    // +govalid:gt=0
    Scores []int `json:"scores"`  // ❌ Each element in numeric slice
    
    // +govalid:email
    Emails []string `json:"emails"`  // ❌ Each element in string slice
    
    // +govalid:maxlength=50
    Names []string `json:"names"`  // ❌ Length constraint for each string
}

Proposed Feature

Numeric Validation

type GameData struct {
    // +govalid:gt=0,lt=100
    Scores []int `json:"scores"`  // Each score in range 0-100
    
    // +govalid:gte=0.0
    Rates []float64 `json:"rates"`  // Each rate >= 0
}

String Validation

type ContactInfo struct {
    // +govalid:email
    Emails []string `json:"emails"`  // Each element is valid email
    
    // +govalid:maxlength=100
    Names []string `json:"names"`  // Each name <= 100 characters
    
    // +govalid:minlength=3,maxlength=20
    Tags []string `json:"tags"`  // Each tag 3-20 characters
}

Required Validation

type StringList struct {
    // +govalid:required
    Items []string `json:"items"`  // Each element is non-zero value
}

Combined with Existing Features

type UserProfile struct {
    // Validate both slice length + each element format
    // +govalid:minitems=1,maxitems=3,email
    Emails []string `json:"emails"`
    
    // +govalid:minitems=1,required,minlength=2
    Tags []string `json:"tags"`
}

Expected Generated Code

Numeric Validation

Input:

type User struct {
    // +govalid:gt=0
    Scores []int `json:"scores"`
}

Generated validation code:

func ValidateUser(t *User) error {
    for _, score := range t.Scores {
        if \!(score > 0) {
            return ErrUserScoresGTValidation
        }
    }
    return nil
}

String Validation

Input:

type Contact struct {
    // +govalid:email
    Emails []string `json:"emails"`
}

Generated validation code:

func ValidateContact(t *Contact) error {
    for _, email := range t.Emails {
        if \!validationhelper.IsValidEmail(email) {
            return ErrContactEmailsEmailValidation
        }
    }
    return nil
}

Target Markers and Types

Numeric Validation Markers

  • Markers: gt, gte, lt, lte
  • Target types: []int, []float64, [N]int32, etc. (all numeric slice/array types)

String Validation Markers

  • Markers: email, maxlength, minlength
  • Target types: []string, [N]string

Generic Validation Markers

  • Markers: required
  • Target types: All slice/array types (zero value check for elements)

Error Messages

// Numeric validation
ErrUserScoresGTValidation = errors.New("all elements in field Scores must be greater than 0")

// String validation  
ErrContactEmailsEmailValidation = errors.New("all elements in field Emails must be valid email addresses")
ErrUserNamesMaxLengthValidation = errors.New("all elements in field Names must be at most 100 characters")

// Required validation
ErrUserTagsRequiredValidation = errors.New("all elements in field Tags are required (cannot be zero value)")

Implementation Considerations

Type Detection Extension

// Current: Only direct types supported
func ValidateGT(pass *codegen.Pass, field *ast.Field, expressions map[string]string, structName string) validator.Validator {
    typ := pass.TypesInfo.TypeOf(field.Type)
    
    // Proposed: Support slice/array element types too
    switch underlyingType := typ.Underlying().(type) {
    case *types.Basic:
        // Existing single field validation
    case *types.Slice:
        elementType := underlyingType.Elem()
        // Validation for element type
    case *types.Array:
        elementType := underlyingType.Elem()
        // Validation for element type
    }
}

Performance

  • Processing time proportional to number of elements
  • Zero allocations maintained
  • Empty slices/arrays skip validation

Special Cases

// Empty slice handling
var emptySlice []string  // Skip validation

// Nil slice handling  
var nilSlice []string = nil  // Skip validation (controlled separately by required)

Usage Examples

type APIRequest struct {
    // +govalid:required
    // +govalid:minitems=1,maxitems=10
    // +govalid:email
    Recipients []string `json:"recipients"`  // Required, 1-10 items, each must be valid email

    // +govalid:minitems=1
    // +govalid:gt=0,lte=100
    Scores []int `json:"scores"`  // At least 1 item, each element 0-100

    // +govalid:maxlength=50,required
    Tags []string `json:"tags"`  // Each element required and <= 50 characters
}

Benefits

  1. Consistency: Extends existing marker syntax naturally
  2. Performance: Compile-time generation maintains zero allocations
  3. Type Safety: Full compile-time validation
  4. Flexibility: Combines with existing collection markers (maxitems/minitems)
  5. Backward Compatibility: Existing single-field usage unchanged

Related Issues

This feature can be combined with existing markers:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions