Go Create Validator
Generate validator files for GO modular architecture conventions.
Two-File Pattern
Every validator requires two files:
-
Port interface: internal/modules/<module>/ports/<validator_name>_validator.go
-
Validator implementation: internal/modules/<module>/validator/<validator_name>_validator.go
Port File Structure
The port file contains only the interface definition with its documentation comment.
Example structure:
package ports
// PasswordValidator validates password strength according to security policies. type PasswordValidator interface { Validate(password string) error }
Validator File Structure
The validator implementation file follows this order:
-
Package imports
-
Constants - validation rules, thresholds, limits
-
Struct definition - the validator implementation struct
-
Interface assertion - compile-time check with var _ ports.XxxValidator = (*XxxValidator)(nil)
-
Constructor - NewXxxValidator function
-
Methods - validation methods (e.g., Validate )
Example structure:
package validator
import ( "github.com/cristiano-pacheco/pingo/internal/modules/<module>/errs" "github.com/cristiano-pacheco/pingo/internal/modules/<module>/ports" )
// 1. Constants const ( minLength = 8 maxLength = 128 )
// 2. Struct definition type PasswordValidator struct{}
// 3. Interface assertion var _ ports.PasswordValidator = (*PasswordValidator)(nil)
// 4. Constructor func NewPasswordValidator() *PasswordValidator { return &PasswordValidator{} }
// 5. Methods func (v *PasswordValidator) Validate(password string) error { // validation logic return nil }
Port Interface Structure
Location: internal/modules/<module>/ports/<validator_name>_validator.go
package ports
// PasswordValidator validates password strength according to security policies. type PasswordValidator interface { Validate(password string) error }
Validator Variants
Stateless validator (no dependencies)
Most validators are stateless utilities with no external dependencies.
type EmailValidator struct{}
func NewEmailValidator() *EmailValidator { return &EmailValidator{} }
func (v *EmailValidator) Validate(email string) error { // Validation logic return nil }
Stateful validator (with dependencies)
Use when validation requires external data or configuration.
type UsernameValidator struct { userRepo ports.UserRepository minLen int maxLen int }
func NewUsernameValidator( userRepo ports.UserRepository, minLen int, maxLen int, ) *UsernameValidator { return &UsernameValidator{ userRepo: userRepo, minLen: minLen, maxLen: maxLen, } }
func (v *UsernameValidator) Validate(ctx context.Context, username string) error { if len(username) < v.minLen { return errs.ErrUsernameTooShort }
// Check uniqueness using repository
exists, err := v.userRepo.ExistsByUsername(ctx, username)
if err != nil {
return err
}
if exists {
return errs.ErrUsernameAlreadyExists
}
return nil
}
Multi-field validator
Use when validation involves multiple related fields.
Port interface:
type RegistrationValidator interface { ValidateEmail(email string) error ValidatePassword(password string) error ValidatePasswordMatch(password, confirmPassword string) error }
Implementation:
type RegistrationValidator struct{}
func NewRegistrationValidator() *RegistrationValidator { return &RegistrationValidator{} }
func (v *RegistrationValidator) ValidateEmail(email string) error { // Email validation logic return nil }
func (v *RegistrationValidator) ValidatePassword(password string) error { // Password validation logic return nil }
func (v *RegistrationValidator) ValidatePasswordMatch(password, confirmPassword string) error { if password != confirmPassword { return errs.ErrPasswordMismatch } return nil }
Validation Constants
Define validation rules as constants at the package level for clarity and maintainability.
const ( minPasswordLength = 8 maxPasswordLength = 128 minUsernameLength = 3 maxUsernameLength = 32 )
Error Handling
Validators MUST return typed domain errors from the module's errs package. When adding new custom errors, translations are mandatory in locale files.
// In internal/modules/<module>/errs/errs.go var ( ErrPasswordTooShort = errors.New("password must be at least 8 characters") ErrPasswordMissingUppercase = errors.New("password must contain at least one uppercase letter") ErrPasswordMissingLowercase = errors.New("password must contain at least one lowercase letter") ErrPasswordMissingDigit = errors.New("password must contain at least one digit") ErrPasswordMissingSpecial = errors.New("password must contain at least one special character") )
For every new custom error added to internal/modules/<module>/errs/errs.go :
-
Add the translation key to locales/en.json
-
Add the same translation key to every other existing locale file (e.g., locales/pt_BR.json )
Context Usage
Validators that perform I/O operations (database lookups, API calls) MUST accept context.Context as the first parameter.
// Stateless validator - no context needed func (v *PasswordValidator) Validate(password string) error
// Stateful validator with I/O - context required func (v *UsernameValidator) Validate(ctx context.Context, username string) error
Naming
-
Port interface: XxxValidator (in ports package)
-
Implementation struct: XxxValidator (in validator package, same name — disambiguated by package)
-
Constructor: NewXxxValidator , returns a pointer of the struct implementation
-
Validation method: Validate for single-purpose validators, or descriptive names for multi-purpose validators
Fx Wiring
Add to internal/modules/<module>/fx.go :
fx.Provide( fx.Annotate( validator.NewPasswordValidator, fx.As(new(ports.PasswordValidator)), ), ),
Dependencies
Validators depend on interfaces only. Common dependencies:
-
ports.XxxRepository — for uniqueness checks or data lookups
-
ports.XxxService — for external validation services
-
Configuration values — passed as constructor parameters
Testing
Validators MUST have comprehensive unit tests covering:
-
Valid input passes validation
-
Each invalid condition returns the correct error
-
Edge cases (empty strings, boundary values, special characters)
Test file location: internal/modules/<module>/validator/<validator_name>_validator_test.go
package validator_test
import ( "testing"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/errs"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/validator"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPasswordValidator_ValidPassword_Passes(t *testing.T) { // Arrange v := validator.NewPasswordValidator()
// Act
err := v.Validate("SecureP@ssw0rd")
// Assert
require.NoError(t, err)
}
func TestPasswordValidator_TooShort_ReturnsError(t *testing.T) { // Arrange v := validator.NewPasswordValidator()
// Act
err := v.Validate("Ab1!")
// Assert
require.Error(t, err)
assert.ErrorIs(t, err, errs.ErrPasswordTooShort)
}
Critical Rules
-
Two files: Port interface in ports/ , implementation in validator/
-
Interface in ports: Interface lives in ports/<name>_validator.go
-
Interface assertion: Add var _ ports.XxxValidator = (*XxxValidator)(nil) below the struct
-
Constructor: MUST return pointer *XxxValidator
-
Stateless by default: Only add dependencies when validation requires external data
-
Context when needed: Accept context.Context only for validators performing I/O
-
Typed errors: Return domain errors from module's errs package
-
Error translations: Every new custom error must have entries in locales/en.json and all other existing locale files
-
Constants: Define validation rules as package-level constants
-
No comments on implementations: Do not add redundant comments above methods in the implementations
-
Add detailed comment on interfaces: Provide comprehensive comments on the port interfaces to describe their purpose and validation rules
-
Comprehensive tests: Test valid cases and all invalid conditions
Workflow
-
Create port interface in ports/<name>_validator.go
-
Create validator implementation in validator/<name>_validator.go
-
Define validation constants
-
Add typed errors to module's errs/errs.go if needed
-
Add translations for each new custom error in locales/en.json and all other existing locale files
-
Create comprehensive unit tests in validator/<name>_validator_test.go
-
Add Fx wiring to module's fx.go
-
Run make test to verify tests pass
-
Run make lint to verify code quality
-
Run make nilaway for static analysis