Validation Patterns

8 minute read

Validation Patterns Overview

Common validation scenarios require consistent, reusable patterns. This chapter covers proven validation strategies for typical use cases.

String Validation Patterns

Email Validation

type emailValidator struct{}

func (v emailValidator) Description(ctx context.Context) string {
    return "value must be a valid email address"
}

func (v emailValidator) MarkdownDescription(ctx context.Context) string {
    return "value must be a valid email address"
}

func (v emailValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
    if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
        return
    }
    
    value := req.ConfigValue.ValueString()
    
    // RFC 5322 compliant email regex (simplified)
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9.!#$%&'*+/=?^_` + "`" + `{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`)
    
    if !emailRegex.MatchString(value) {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Invalid Email Address",
            fmt.Sprintf(
                "'%s' is not a valid email address. "+
                "Email must be in format: user@example.com",
                value,
            ),
        )
    }
}

func Email() validator.String {
    return emailValidator{}
}

// Usage
"notification_email": schema.StringAttribute{
    Optional: true,
    Validators: []validator.String{
        Email(),
    },
},

URL Validation

type urlValidator struct {
    schemes []string
}

func (v urlValidator) Description(ctx context.Context) string {
    if len(v.schemes) > 0 {
        return fmt.Sprintf("value must be a valid URL with scheme: %v", v.schemes)
    }
    return "value must be a valid URL"
}

func (v urlValidator) MarkdownDescription(ctx context.Context) string {
    return v.Description(ctx)
}

func (v urlValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
    if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
        return
    }
    
    value := req.ConfigValue.ValueString()
    
    parsedURL, err := url.Parse(value)
    if err != nil {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Invalid URL",
            fmt.Sprintf("'%s' is not a valid URL: %s", value, err.Error()),
        )
        return
    }
    
    if parsedURL.Scheme == "" {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Missing URL Scheme",
            fmt.Sprintf(
                "URL '%s' must include a scheme (e.g., https://). "+
                "Example: https://example.com",
                value,
            ),
        )
        return
    }
    
    // Validate specific schemes if provided
    if len(v.schemes) > 0 {
        validScheme := false
        for _, scheme := range v.schemes {
            if parsedURL.Scheme == scheme {
                validScheme = true
                break
            }
        }
        
        if !validScheme {
            resp.Diagnostics.AddAttributeError(
                req.Path,
                "Invalid URL Scheme",
                fmt.Sprintf(
                    "URL must use one of these schemes: %v, got: %s",
                    v.schemes,
                    parsedURL.Scheme,
                ),
            )
        }
    }
}

func URL(schemes ...string) validator.String {
    return urlValidator{schemes: schemes}
}

// Usage
"webhook_url": schema.StringAttribute{
    Required: true,
    Validators: []validator.String{
        URL("https"),  // Only HTTPS
    },
},
"endpoint": schema.StringAttribute{
    Required: true,
    Validators: []validator.String{
        URL("http", "https"),  // HTTP or HTTPS
    },
},

IPv4/IPv6 Validation

type ipAddressValidator struct {
    v4Only bool
    v6Only bool
}

func (v ipAddressValidator) Description(ctx context.Context) string {
    if v.v4Only {
        return "value must be a valid IPv4 address"
    }
    if v.v6Only {
        return "value must be a valid IPv6 address"
    }
    return "value must be a valid IP address"
}

func (v ipAddressValidator) MarkdownDescription(ctx context.Context) string {
    return v.Description(ctx)
}

func (v ipAddressValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
    if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
        return
    }
    
    value := req.ConfigValue.ValueString()
    
    ip := net.ParseIP(value)
    if ip == nil {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Invalid IP Address",
            fmt.Sprintf("'%s' is not a valid IP address", value),
        )
        return
    }
    
    if v.v4Only && ip.To4() == nil {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "IPv4 Address Required",
            fmt.Sprintf("'%s' is not a valid IPv4 address", value),
        )
    }
    
    if v.v6Only && ip.To4() != nil {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "IPv6 Address Required",
            fmt.Sprintf("'%s' is not a valid IPv6 address", value),
        )
    }
}

func IPv4() validator.String {
    return ipAddressValidator{v4Only: true}
}

func IPv6() validator.String {
    return ipAddressValidator{v6Only: true}
}

func IPAddress() validator.String {
    return ipAddressValidator{}
}

CIDR Validation

type cidrValidator struct{}

func (v cidrValidator) Description(ctx context.Context) string {
    return "value must be a valid CIDR block"
}

func (v cidrValidator) MarkdownDescription(ctx context.Context) string {
    return "value must be a valid CIDR block (e.g., `10.0.0.0/16`)"
}

func (v cidrValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
    if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
        return
    }
    
    value := req.ConfigValue.ValueString()
    
    _, _, err := net.ParseCIDR(value)
    if err != nil {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Invalid CIDR Block",
            fmt.Sprintf(
                "'%s' is not a valid CIDR block. "+
                "CIDR blocks must be in format: IP/prefix (e.g., 10.0.0.0/16)",
                value,
            ),
        )
    }
}

func CIDR() validator.String {
    return cidrValidator{}
}

Number Validation Patterns

Port Range Validation

func PortNumber() validator.Int64 {
    return int64validator.All(
        int64validator.Between(1, 65535),
    )
}

func UnprivilegedPort() validator.Int64 {
    return int64validator.All(
        int64validator.Between(1024, 65535),
    )
}

func WellKnownPort() validator.Int64 {
    return int64validator.All(
        int64validator.Between(1, 1023),
    )
}

// Usage
"port": schema.Int64Attribute{
    Optional: true,
    Validators: []validator.Int64{
        UnprivilegedPort(),
    },
},

Percentage Validation

type percentageValidator struct{}

func (v percentageValidator) Description(ctx context.Context) string {
    return "value must be between 0 and 100"
}

func (v percentageValidator) MarkdownDescription(ctx context.Context) string {
    return "value must be between 0 and 100 (percentage)"
}

func (v percentageValidator) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) {
    if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
        return
    }
    
    value := req.ConfigValue.ValueInt64()
    
    if value < 0 || value > 100 {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Invalid Percentage",
            fmt.Sprintf(
                "Value must be between 0 and 100, got: %d",
                value,
            ),
        )
    }
}

func Percentage() validator.Int64 {
    return percentageValidator{}
}

List Validation Patterns

Unique Values

"availability_zones": schema.ListAttribute{
    ElementType: types.StringType,
    Required:    true,
    Validators: []validator.List{
        listvalidator.SizeBetween(2, 3),
        listvalidator.UniqueValues(),
    },
},

List Element Validation

"security_group_ids": schema.ListAttribute{
    ElementType: types.StringType,
    Optional:    true,
    Validators: []validator.List{
        listvalidator.SizeAtLeast(1),
        listvalidator.ValueStringsAre(
            stringvalidator.RegexMatches(
                regexp.MustCompile(`^sg-[a-f0-9]{8,17}$`),
                "must be a valid security group ID",
            ),
        ),
    },
},

Map Validation Patterns

Map Key Validation

"tags": schema.MapAttribute{
    ElementType: types.StringType,
    Optional:    true,
    Validators: []validator.Map{
        mapvalidator.SizeAtMost(50),
        mapvalidator.KeysAre(
            stringvalidator.LengthBetween(1, 128),
            stringvalidator.RegexMatches(
                regexp.MustCompile(`^[a-zA-Z0-9\-_:]+$`),
                "tag keys can only contain letters, numbers, hyphens, underscores, and colons",
            ),
        ),
    },
},

Map Value Validation

"environment_variables": schema.MapAttribute{
    ElementType: types.StringType,
    Optional:    true,
    Validators: []validator.Map{
        mapvalidator.ValueStringsAre(
            stringvalidator.LengthAtMost(1000),
        ),
    },
},

Conditional Validation

At Least One Required

func (r *Resource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
    var data Model
    resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
    
    // At least one of these must be specified
    if data.PasswordSecretARN.IsNull() && data.Password.IsNull() {
        resp.Diagnostics.AddError(
            "Missing Required Configuration",
            "Either 'password' or 'password_secret_arn' must be specified",
        )
    }
}

Mutually Exclusive

func (r *Resource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
    var data Model
    resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
    
    // Only one can be specified
    count := 0
    if !data.PublicAccess.IsNull() && data.PublicAccess.ValueBool() {
        count++
    }
    if !data.VpcID.IsNull() {
        count++
    }
    if !data.PrivateLink.IsNull() && data.PrivateLink.ValueBool() {
        count++
    }
    
    if count > 1 {
        resp.Diagnostics.AddError(
            "Conflicting Configuration",
            "Only one of 'public_access', 'vpc_id', or 'private_link' can be specified",
        )
    }
    
    if count == 0 {
        resp.Diagnostics.AddError(
            "Missing Required Configuration",
            "One of 'public_access', 'vpc_id', or 'private_link' must be specified",
        )
    }
}

Dependent Attributes

func (r *Resource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
    var data Model
    resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
    
    // If backup is enabled, these are required
    if !data.BackupEnabled.IsNull() && data.BackupEnabled.ValueBool() {
        if data.BackupRetentionDays.IsNull() {
            resp.Diagnostics.AddAttributeError(
                path.Root("backup_retention_days"),
                "Required When Backup Enabled",
                "backup_retention_days is required when backup_enabled is true",
            )
        }
        
        if data.BackupWindow.IsNull() {
            resp.Diagnostics.AddAttributeError(
                path.Root("backup_window"),
                "Required When Backup Enabled",
                "backup_window is required when backup_enabled is true",
            )
        }
    }
}

Complex Validation Patterns

Time Window Validation

type timeWindowValidator struct{}

func (v timeWindowValidator) Description(ctx context.Context) string {
    return "value must be a valid time window in format 'hh24:mi-hh24:mi'"
}

func (v timeWindowValidator) MarkdownDescription(ctx context.Context) string {
    return "value must be a valid time window in format `hh24:mi-hh24:mi` (e.g., `03:00-05:00`)"
}

func (v timeWindowValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
    if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
        return
    }
    
    value := req.ConfigValue.ValueString()
    
    // Format: HH:MM-HH:MM
    regex := regexp.MustCompile(`^([0-1][0-9]|2[0-3]):[0-5][0-9]-([0-1][0-9]|2[0-3]):[0-5][0-9]$`)
    if !regex.MatchString(value) {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Invalid Time Window",
            fmt.Sprintf(
                "Time window must be in format 'hh24:mi-hh24:mi' (e.g., '03:00-05:00'), got: %s",
                value,
            ),
        )
        return
    }
    
    // Parse start and end times
    parts := strings.Split(value, "-")
    startParts := strings.Split(parts[0], ":")
    endParts := strings.Split(parts[1], ":")
    
    startHour, _ := strconv.Atoi(startParts[0])
    startMin, _ := strconv.Atoi(startParts[1])
    endHour, _ := strconv.Atoi(endParts[0])
    endMin, _ := strconv.Atoi(endParts[1])
    
    startMinutes := startHour*60 + startMin
    endMinutes := endHour*60 + endMin
    
    // Validate window is at least 30 minutes
    duration := endMinutes - startMinutes
    if duration < 30 {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Time Window Too Short",
            fmt.Sprintf(
                "Time window must be at least 30 minutes, got: %d minutes",
                duration,
            ),
        )
    }
}

func TimeWindow() validator.String {
    return timeWindowValidator{}
}

JSON Validation

type jsonValidator struct{}

func (v jsonValidator) Description(ctx context.Context) string {
    return "value must be valid JSON"
}

func (v jsonValidator) MarkdownDescription(ctx context.Context) string {
    return "value must be valid JSON"
}

func (v jsonValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
    if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
        return
    }
    
    value := req.ConfigValue.ValueString()
    
    var js json.RawMessage
    if err := json.Unmarshal([]byte(value), &js); err != nil {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Invalid JSON",
            fmt.Sprintf("Value is not valid JSON: %s", err.Error()),
        )
    }
}

func JSON() validator.String {
    return jsonValidator{}
}

Reusable Validation Helpers

Create Validation Library

// internal/validators/validators.go
package validators

// Common validators
func ResourceName() validator.String {
    return stringvalidator.All(
        stringvalidator.LengthBetween(3, 63),
        stringvalidator.RegexMatches(
            regexp.MustCompile(`^[a-z][a-z0-9-]*$`),
            "must start with lowercase letter and contain only lowercase letters, numbers, and hyphens",
        ),
    )
}

func ARN() validator.String {
    return stringvalidator.RegexMatches(
        regexp.MustCompile(`^arn:aws:[a-z\-]+:[a-z0-9\-]*:\d{12}:[a-z\-\/]+$`),
        "must be a valid ARN",
    )
}

func SecurityGroupID() validator.String {
    return stringvalidator.RegexMatches(
        regexp.MustCompile(`^sg-[a-f0-9]{8,17}$`),
        "must be a valid security group ID (e.g., sg-0123456789abcdef0)",
    )
}

// Usage across resources
"name": schema.StringAttribute{
    Required: true,
    Validators: []validator.String{
        validators.ResourceName(),
    },
},

Best Practices

1. Validate Early

// ✅ Good: Schema-level validation
"port": schema.Int64Attribute{
    Optional: true,
    Validators: []validator.Int64{
        int64validator.Between(1024, 65535),
    },
}

// ❌ Bad: Validate in Create method
func (r *Resource) Create(...) {
    if port < 1024 {
        resp.Diagnostics.AddError(...)
    }
}

2. Create Reusable Validators

// ✅ Good: Reusable validator
func ResourceName() validator.String {
    return stringvalidator.All(
        stringvalidator.LengthBetween(3, 63),
        stringvalidator.RegexMatches(...),
    )
}

// Use everywhere
"cache_name": schema.StringAttribute{
    Validators: []validator.String{ResourceName()},
},
"cluster_name": schema.StringAttribute{
    Validators: []validator.String{ResourceName()},
},

3. Provide Clear Messages

// ✅ Good: Clear message with example
stringvalidator.RegexMatches(
    regexp.MustCompile(`^[a-z][a-z0-9-]*$`),
    "must start with lowercase letter and contain only lowercase letters, numbers, and hyphens. Example: 'my-cache-01'",
)

// ❌ Bad: Vague message
stringvalidator.RegexMatches(
    regexp.MustCompile(`^[a-z][a-z0-9-]*$`),
    "invalid format",
)

Key Takeaways

  • ✅ Create reusable validators for common patterns
  • ✅ Validate emails, URLs, IPs, CIDRs properly
  • ✅ Use schema validators for early validation
  • ✅ Build a validation library for your provider
  • ✅ Handle conditional validation scenarios
  • ✅ Provide clear error messages with examples
  • ✅ Test validators with valid and invalid inputs
  • ✅ Combine multiple validators when needed
  • ✅ Document validation rules in schema
  • ✅ Keep validation logic consistent

What’s Next?

Learn how to handle API errors gracefully and translate them for users.

Next: API Error Handling →


Previous: ← Diagnostic Messages

Last updated: June 30, 2026

Updated: