Validation Patterns
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.
Previous: ← Diagnostic Messages
Last updated: June 30, 2026