Validation and Constraints
Validation Overview
Validation is one of the most important aspects of provider development. Good validation catches configuration errors during terraform plan, not during terraform apply when it’s too late.
In this chapter, you’ll learn how to implement comprehensive validation using the Plugin Framework’s validator system.
Why Validate?
Benefits of validation:
- ✅ Early error detection — Catch problems at plan time
- ✅ Better error messages — Guide users to correct configuration
- ✅ Prevent API errors — Don’t waste API calls on invalid input
- ✅ Type safety — Enforce business rules beyond basic types
- ✅ User experience — Fast feedback loop
Without validation:
$ terraform apply
...
Error: API returned 400: Invalid port number 99999
With validation:
$ terraform plan
...
Error: Invalid Attribute Value
Port must be between 1 and 65535, got 99999
Built-in Validators
The Plugin Framework provides validators for all common scenarios.
String Validators
import (
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)
"region": schema.StringAttribute{
Required: true,
Validators: []validator.String{
// One of specific values
stringvalidator.OneOf("us-east-1", "us-west-2", "eu-west-1"),
},
},
Available string validators:
OneOf
stringvalidator.OneOf("value1", "value2", "value3")
NoneOf
stringvalidator.NoneOf("forbidden1", "forbidden2")
LengthBetween
stringvalidator.LengthBetween(1, 255)
LengthAtLeast
stringvalidator.LengthAtLeast(1)
LengthAtMost
stringvalidator.LengthAtMost(100)
RegexMatches
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-z][a-z0-9-]*$`),
"must start with letter and contain only lowercase letters, numbers, and hyphens",
)
UTF8LengthBetween
stringvalidator.UTF8LengthBetween(1, 100)
ConflictsWith
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot("other_field"),
}...)
AlsoRequires
stringvalidator.AlsoRequires(path.Expressions{
path.MatchRoot("required_field"),
}...)
Number Validators
import "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator"
"port": schema.NumberAttribute{
Required: true,
Validators: []validator.Number{
numbervalidator.Between(1, 65535),
},
},
Available number validators:
Between
numbervalidator.Between(1, 100)
AtLeast
numbervalidator.AtLeast(0)
AtMost
numbervalidator.AtMost(1000)
OneOf
numbervalidator.OneOf(
big.NewFloat(1),
big.NewFloat(2),
big.NewFloat(4),
big.NewFloat(8),
)
NoneOf
numbervalidator.NoneOf(
big.NewFloat(0),
big.NewFloat(-1),
)
Bool Validators
import "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator"
"enabled": schema.BoolAttribute{
Required: true,
Validators: []validator.Bool{
// Example: Conflicts with another field
boolvalidator.ConflictsWith(path.Expressions{
path.MatchRoot("disabled"),
}...),
},
},
Available bool validators:
ConflictsWith
boolvalidator.ConflictsWith(path.Expressions{...}...)
AlsoRequires
boolvalidator.AlsoRequires(path.Expressions{...}...)
List Validators
import "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"availability_zones": schema.ListAttribute{
ElementType: types.StringType,
Required: true,
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
listvalidator.SizeAtMost(3),
listvalidator.UniqueValues(),
},
},
Available list validators:
SizeBetween
listvalidator.SizeBetween(1, 10)
SizeAtLeast
listvalidator.SizeAtLeast(1)
SizeAtMost
listvalidator.SizeAtMost(5)
UniqueValues
listvalidator.UniqueValues()
ValueStringsAre (nested validation)
listvalidator.ValueStringsAre(
stringvalidator.LengthAtLeast(1),
)
Map Validators
import "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
"labels": schema.MapAttribute{
ElementType: types.StringType,
Optional: true,
Validators: []validator.Map{
mapvalidator.SizeAtMost(50),
mapvalidator.KeysAre(
stringvalidator.LengthBetween(1, 63),
),
},
},
Available map validators:
SizeBetween
mapvalidator.SizeBetween(1, 100)
SizeAtLeast
mapvalidator.SizeAtLeast(1)
SizeAtMost
mapvalidator.SizeAtMost(50)
KeysAre (nested validation)
mapvalidator.KeysAre(
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`),
"must start with letter",
),
)
ValueStringsAre (nested validation)
mapvalidator.ValueStringsAre(
stringvalidator.LengthAtMost(256),
)
Set Validators
import "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"security_groups": schema.SetAttribute{
ElementType: types.StringType,
Required: true,
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
setvalidator.ValueStringsAre(
stringvalidator.RegexMatches(
regexp.MustCompile(`^sg-[a-z0-9]+$`),
"must be valid security group ID",
),
),
},
},
Available set validators:
SizeBetween
setvalidator.SizeBetween(1, 10)
SizeAtLeast
setvalidator.SizeAtLeast(1)
SizeAtMost
setvalidator.SizeAtMost(5)
ValueStringsAre (nested validation)
setvalidator.ValueStringsAre(
stringvalidator.LengthAtLeast(1),
)
Multiple Validators
Combine multiple validators on a single attribute:
"name": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.LengthBetween(3, 64),
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-z][a-z0-9-]*$`),
"must start with lowercase letter and contain only lowercase letters, numbers, and hyphens",
),
},
},
All validators must pass.
Custom Validators
Create custom validators for business logic.
String Validator Example
package validators
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)
type noWhitespaceValidator struct{}
func NoWhitespace() validator.String {
return noWhitespaceValidator{}
}
func (v noWhitespaceValidator) Description(ctx context.Context) string {
return "value must not contain whitespace"
}
func (v noWhitespaceValidator) MarkdownDescription(ctx context.Context) string {
return "value must not contain whitespace"
}
func (v noWhitespaceValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
value := req.ConfigValue.ValueString()
if strings.Contains(value, " ") || strings.Contains(value, "\t") || strings.Contains(value, "\n") {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid Value",
fmt.Sprintf("Value must not contain whitespace, got: %q", value),
)
}
}
Usage:
"username": schema.StringAttribute{
Required: true,
Validators: []validator.String{
validators.NoWhitespace(),
stringvalidator.LengthBetween(3, 32),
},
},
Number Validator Example
type portRangeValidator struct{}
func PortRange() validator.Number {
return portRangeValidator{}
}
func (v portRangeValidator) Description(ctx context.Context) string {
return "value must be a valid port number (1-65535)"
}
func (v portRangeValidator) MarkdownDescription(ctx context.Context) string {
return "value must be a valid port number (1-65535)"
}
func (v portRangeValidator) ValidateNumber(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
value := req.ConfigValue.ValueBigFloat()
port, _ := value.Int64()
if port < 1 || port > 65535 {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid Port Number",
fmt.Sprintf("Port must be between 1 and 65535, got: %d", port),
)
}
}
Cross-Attribute Validation
Validate relationships between multiple attributes.
Using Built-in Validators
ConflictsWith
"password": schema.StringAttribute{
Optional: true,
Sensitive: true,
Validators: []validator.String{
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot("password_hash"),
}...),
},
},
"password_hash": schema.StringAttribute{
Optional: true,
Sensitive: true,
Validators: []validator.String{
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot("password"),
}...),
},
},
AlsoRequires
"vpc_id": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.AlsoRequires(path.Expressions{
path.MatchRoot("subnet_id"),
}...),
},
},
"subnet_id": schema.StringAttribute{
Optional: true,
},
AtLeastOneOf
"public_key": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.AtLeastOneOf(path.Expressions{
path.MatchRoot("public_key"),
path.MatchRoot("public_key_path"),
}...),
},
},
"public_key_path": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.AtLeastOneOf(path.Expressions{
path.MatchRoot("public_key"),
path.MatchRoot("public_key_path"),
}...),
},
},
Custom Cross-Attribute Validation
For complex logic, use plan validators:
func (r *MyResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var config MyResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Custom validation logic
if !config.MinReplicas.IsNull() && !config.MaxReplicas.IsNull() {
minVal, _ := config.MinReplicas.ValueBigFloat().Int64()
maxVal, _ := config.MaxReplicas.ValueBigFloat().Int64()
if minVal > maxVal {
resp.Diagnostics.AddError(
"Invalid Configuration",
fmt.Sprintf("min_replicas (%d) must be less than or equal to max_replicas (%d)", minVal, maxVal),
)
}
}
}
Validation Best Practices
1. Validate Early and Often
// ✅ Good: Validate in schema
"port": schema.NumberAttribute{
Required: true,
Validators: []validator.Number{
numbervalidator.Between(1, 65535),
},
},
// ❌ Bad: Validate in Create
func (r *MyResource) Create(...) {
// Too late! Should be in schema
if port < 1 || port > 65535 {
return errors.New("invalid port")
}
}
2. Provide Clear Error Messages
// ✅ Good: Clear message
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-z][a-z0-9-]*$`),
"must start with lowercase letter and contain only lowercase letters, numbers, and hyphens",
)
// ❌ Bad: Vague message
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-z][a-z0-9-]*$`),
"invalid format",
)
3. Validate All Constraints
// ✅ Good: Complete validation
"name": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.LengthBetween(1, 255), // Length constraint
stringvalidator.RegexMatches(...), // Format constraint
validators.NoWhitespace(), // Business rule
},
},
4. Document Validation Rules
"name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Environment name. Must be 1-255 characters, start with lowercase letter, and contain only lowercase letters, numbers, and hyphens.",
Validators: []validator.String{
stringvalidator.LengthBetween(1, 255),
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-z][a-z0-9-]*$`),
"must start with lowercase letter and contain only lowercase letters, numbers, and hyphens",
),
},
},
Common Validation Patterns
Pattern 1: Enum Values
"instance_type": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("t2.micro", "t2.small", "t2.medium", "t2.large"),
},
},
Pattern 2: Resource Identifier Format
"vpc_id": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^vpc-[a-z0-9]+$`),
"must be valid VPC ID (e.g., vpc-abc123)",
),
},
},
Pattern 3: CIDR Blocks
"cidr_block": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$`),
"must be valid CIDR block (e.g., 10.0.0.0/16)",
),
},
},
Pattern 4: Email Addresses
"email": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`),
"must be valid email address",
),
},
},
Pattern 5: URL Validation
"endpoint": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^https?://`),
"must be valid HTTP or HTTPS URL",
),
},
},
Pattern 6: Range Validation
"cpu_percentage": schema.NumberAttribute{
Required: true,
Validators: []validator.Number{
numbervalidator.Between(0, 100),
},
},
Pattern 7: Mutually Exclusive Fields
"source_ami": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.ExactlyOneOf(path.Expressions{
path.MatchRoot("source_ami"),
path.MatchRoot("source_snapshot"),
}...),
},
},
"source_snapshot": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.ExactlyOneOf(path.Expressions{
path.MatchRoot("source_ami"),
path.MatchRoot("source_snapshot"),
}...),
},
},
Error Messages
Good Error Message Structure
Error: Invalid Attribute Value
on main.tf line 10, in resource "mycloud_environment" "example":
10: port = 99999
Port must be between 1 and 65535, got 99999.
Components:
- Error type
- Location (file, line, resource)
- Invalid value
- Clear explanation of constraint
- Actual value received
Error Message Best Practices
✅ Good:
- “Port must be between 1 and 65535, got 99999”
- “Name must start with lowercase letter and contain only lowercase letters, numbers, and hyphens”
- “Either public_key or public_key_path must be specified, but not both”
❌ Bad:
- “Invalid value”
- “Wrong format”
- “Error”
Key Takeaways
- ✅ Validate at plan time, not apply time
- ✅ Use built-in validators when possible
- ✅ Create custom validators for business logic
- ✅ Validate cross-attribute constraints
- ✅ Provide clear, actionable error messages
- ✅ Document validation rules in schema descriptions
- ✅ Test validators thoroughly
What’s Next?
Now let’s learn about handling sensitive attributes.
Previous: ← Required, Optional, and Computed
Last updated: June 30, 2026