Validation
fig supports custom validation through the Validator interface. This guide covers implementation patterns and best practices.
The Validator Interface
type Validator interface {
Validate() error
}
If your config struct implements Validator, fig calls Validate() after all fields are populated. Validation errors are returned directly from Load.
Basic Validation
Range checks and simple constraints:
type ServerConfig struct {
Port int `env:"PORT" default:"8080"`
Timeout int `env:"TIMEOUT" default:"30"`
Host string `env:"HOST" default:"0.0.0.0"`
}
func (c *ServerConfig) Validate() error {
if c.Port <= 0 || c.Port > 65535 {
return errors.New("port must be between 1 and 65535")
}
if c.Timeout <= 0 {
return errors.New("timeout must be positive")
}
return nil
}
Cross-Field Validation
Validate relationships between fields:
type PoolConfig struct {
MinConns int `env:"POOL_MIN" default:"5"`
MaxConns int `env:"POOL_MAX" default:"25"`
}
func (c *PoolConfig) Validate() error {
if c.MinConns > c.MaxConns {
return fmt.Errorf("min connections (%d) cannot exceed max (%d)",
c.MinConns, c.MaxConns)
}
if c.MinConns < 0 {
return errors.New("min connections cannot be negative")
}
return nil
}
Multiple Errors
Collect all validation errors before returning:
import (
"errors"
"fmt"
"strings"
)
type Config struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
LogLevel string `env:"LOG_LEVEL"`
}
func (c *Config) Validate() error {
var errs []string
if c.Host == "" {
errs = append(errs, "host is required")
}
if c.Port <= 0 || c.Port > 65535 {
errs = append(errs, "port must be between 1 and 65535")
}
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
if c.LogLevel != "" && !validLevels[c.LogLevel] {
errs = append(errs, fmt.Sprintf("invalid log level: %s", c.LogLevel))
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "; "))
}
return nil
}
Validation vs Required
Use required:"true" for presence checks, Validate() for semantic checks:
type Config struct {
// Use required for "must be set"
APIKey string `env:"API_KEY" required:"true"`
// Use validation for "must be valid format"
Email string `env:"ADMIN_EMAIL"`
}
func (c *Config) Validate() error {
if c.Email != "" && !strings.Contains(c.Email, "@") {
return errors.New("admin email must be valid")
}
return nil
}
When to use required:
- The field must have a non-zero value
- Absence is a configuration error
When to use Validate:
- The value must meet specific criteria
- Cross-field dependencies exist
- Format validation is needed
Conditional Validation
Validate based on other field values:
type Config struct {
Mode string `env:"MODE" default:"development"`
TLSCert string `env:"TLS_CERT"`
TLSKey string `env:"TLS_KEY"`
}
func (c *Config) Validate() error {
if c.Mode == "production" {
if c.TLSCert == "" || c.TLSKey == "" {
return errors.New("TLS cert and key required in production mode")
}
}
return nil
}
Nested Struct Validation
Each nested struct can implement its own Validator:
type DatabaseConfig struct {
Host string `env:"DB_HOST" default:"localhost"`
Port int `env:"DB_PORT" default:"5432"`
}
func (c *DatabaseConfig) Validate() error {
if c.Port <= 0 {
return errors.New("database port must be positive")
}
return nil
}
type AppConfig struct {
Database DatabaseConfig
Name string `env:"APP_NAME" required:"true"`
}
func (c *AppConfig) Validate() error {
// Explicitly validate nested struct
if err := c.Database.Validate(); err != nil {
return fmt.Errorf("database config: %w", err)
}
if len(c.Name) > 64 {
return errors.New("app name too long")
}
return nil
}
Note: fig only calls Validate() on the top-level struct. Nested validation must be called explicitly.
External Validation Libraries
Integrate with validation libraries:
import "github.com/go-playground/validator/v10"
var validate = validator.New()
type Config struct {
Email string `env:"EMAIL" validate:"required,email"`
URL string `env:"URL" validate:"required,url"`
}
func (c *Config) Validate() error {
return validate.Struct(c)
}
Testing Validation
Test validation logic independently:
func TestConfigValidation(t *testing.T) {
tests := []struct {
name string
config Config
wantErr bool
}{
{
name: "valid config",
config: Config{Port: 8080, Host: "localhost"},
wantErr: false,
},
{
name: "invalid port",
config: Config{Port: -1, Host: "localhost"},
wantErr: true,
},
{
name: "port out of range",
config: Config{Port: 70000, Host: "localhost"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Error Handling
Validation errors are not wrapped in FieldError:
err := fig.Load(&cfg)
if err != nil {
var fieldErr *fig.FieldError
if errors.As(err, &fieldErr) {
// This is a field-level error (required, type conversion)
fmt.Printf("Field %s: %v\n", fieldErr.Field, fieldErr.Err)
} else {
// This is a validation error or other error
fmt.Printf("Validation: %v\n", err)
}
}
Next Steps
- Troubleshooting — common errors and debugging
- Testing — testing patterns for fig
- Reference — full API documentation