Schema Best Practices
Schema Design Principles
Well-designed schemas are:
- Intuitive — Users understand configuration without reading docs
- Consistent — Similar resources use similar patterns
- Backwards compatible — Changes don’t break existing configurations
- Validated — Errors caught at plan time
- Documented — Clear descriptions for all attributes
Naming Conventions
Use snake_case
// ✅ Good: snake_case
"instance_type": schema.StringAttribute{...}
"max_retries": schema.NumberAttribute{...}
"connection_timeout": schema.NumberAttribute{...}
// ❌ Bad: camelCase or PascalCase
"instanceType": schema.StringAttribute{...}
"MaxRetries": schema.NumberAttribute{...}
Be Descriptive
// ✅ Good: Clear and specific
"health_check_interval_seconds": schema.NumberAttribute{...}
"minimum_healthy_instances": schema.NumberAttribute{...}
// ❌ Bad: Vague or unclear
"interval": schema.NumberAttribute{...}
"min": schema.NumberAttribute{...}
Avoid Redundancy
// ✅ Good: Context from resource type
resource "mycloud_database" "example" {
name = "mydb" // Clear it's database name
}
// ❌ Bad: Redundant prefix
resource "mycloud_database" "example" {
database_name = "mydb" // "database_" is redundant
}
Use Common Conventions
// ✅ Good: Standard attribute names
"id": schema.StringAttribute{
Computed: true,
}
"name": schema.StringAttribute{
Required: true,
}
"arn": schema.StringAttribute{
Computed: true,
}
"tags": schema.MapAttribute{
ElementType: types.StringType,
Optional: true,
}
"created_at": schema.StringAttribute{
Computed: true,
}
"updated_at": schema.StringAttribute{
Computed: true,
}
Boolean Naming
// ✅ Good: Positive boolean names
"enabled": schema.BoolAttribute{...}
"auto_scaling": schema.BoolAttribute{...}
"monitoring": schema.BoolAttribute{...}
// ❌ Bad: Negative boolean names (confusing)
"disabled": schema.BoolAttribute{...} // enabled = !disabled?
"no_auto_scaling": schema.BoolAttribute{...}
Attribute Organization
Group Related Attributes
// ✅ Good: Grouped in nested block
"network": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"vpc_id": schema.StringAttribute{...},
"subnet_id": schema.StringAttribute{...},
"security_group_ids": schema.ListAttribute{...},
},
},
// ❌ Bad: Scattered at top level
"network_vpc_id": schema.StringAttribute{...}
"network_subnet_id": schema.StringAttribute{...}
"network_security_group_ids": schema.ListAttribute{...}
Logical Ordering
// ✅ Good: Logical order
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
// Identity (first)
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Required: true,
},
// Configuration (middle)
"instance_type": schema.StringAttribute{...},
"region": schema.StringAttribute{...},
// Computed metadata (last)
"arn": schema.StringAttribute{
Computed: true,
},
"created_at": schema.StringAttribute{
Computed: true,
},
},
}
Required vs Optional
Minimize Required Attributes
// ✅ Good: Only essentials required
"name": schema.StringAttribute{
Required: true, // Can't create without name
},
"description": schema.StringAttribute{
Optional: true, // Nice to have, not essential
},
"region": schema.StringAttribute{
Optional: true,
Computed: true, // Default if not provided
},
Provide Sensible Defaults
// ✅ Good: Defaults for optional fields
"port": schema.NumberAttribute{
Optional: true,
Computed: true,
Default: numberdefault.StaticNumber(big.NewFloat(8080)),
},
"timeout": schema.NumberAttribute{
Optional: true,
Computed: true,
Default: numberdefault.StaticNumber(big.NewFloat(30)),
},
"enabled": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
Backwards Compatibility
Never Remove Attributes
// ❌ Bad: Breaks existing configs
// v1.0
"instance_type": schema.StringAttribute{
Required: true,
}
// v2.0 - DON'T DO THIS!
// "instance_type" removed ← BREAKING CHANGE!
Deprecate Instead of Remove
// ✅ Good: Deprecate, don't remove
"instance_type": schema.StringAttribute{
Optional: true,
DeprecationMessage: "Use 'instance_class' instead. This attribute will be removed in v3.0",
},
"instance_class": schema.StringAttribute{
Optional: true,
},
Don’t Change Attribute Types
// ❌ Bad: Type change breaks configs
// v1.0
"port": schema.NumberAttribute{...}
// v2.0 - DON'T DO THIS!
"port": schema.StringAttribute{...} ← BREAKING CHANGE!
Adding Optional Attributes is Safe
// ✅ Good: Safe to add optional attributes
// v1.0
schema.Schema{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
},
}
// v2.0 - Safe addition
schema.Schema{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"description": schema.StringAttribute{
Optional: true, // ← Safe to add
},
},
}
Changing Optional to Required is Breaking
// ❌ Bad: Making optional required breaks configs
// v1.0
"description": schema.StringAttribute{
Optional: true,
}
// v2.0 - DON'T DO THIS!
"description": schema.StringAttribute{
Required: true, // ← BREAKING CHANGE!
}
Validation Best Practices
Validate in Schema
// ✅ Good: Validation in schema
"port": schema.NumberAttribute{
Required: true,
Validators: []validator.Number{
numbervalidator.Between(1, 65535),
},
},
// ❌ Bad: Validation in Create (too late!)
func (r *MyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
if port < 1 || port > 65535 {
resp.Diagnostics.AddError("Invalid port", "...")
return
}
}
Provide Clear Error Messages
// ✅ Good: Helpful error 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 error message
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-z][a-z0-9-]*$`),
"invalid format",
)
Document Validation Rules
// ✅ Good: Documented constraints
"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",
),
},
},
Documentation
Markdown Descriptions
// ✅ Good: Clear, comprehensive description
"timeout": schema.NumberAttribute{
Optional: true,
Computed: true,
MarkdownDescription: "Connection timeout in seconds. Must be between 1 and 300. Defaults to 30.",
Validators: []validator.Number{
numbervalidator.Between(1, 300),
},
Default: numberdefault.StaticNumber(big.NewFloat(30)),
},
// ❌ Bad: No description
"timeout": schema.NumberAttribute{
Optional: true,
Computed: true,
},
Document Computed Attributes
// ✅ Good: Explains what's computed
"endpoint": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The endpoint URL of the service. Automatically generated after creation.",
},
"status": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Current status of the resource. Possible values: 'creating', 'active', 'deleting', 'failed'.",
},
Document Sensitive Attributes
// ✅ Good: Warns about sensitivity
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
MarkdownDescription: "Database password. Must be at least 8 characters. This value is sensitive and will not be displayed in logs. Note: stored in plaintext in state file.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(8),
},
},
Common Anti-Patterns
❌ Anti-Pattern 1: Stringly-Typed Everything
// Bad: Port as string
"port": schema.StringAttribute{
Required: true,
}
// Good: Port as number
"port": schema.NumberAttribute{
Required: true,
Validators: []validator.Number{
numbervalidator.Between(1, 65535),
},
},
❌ Anti-Pattern 2: Magic Values
// Bad: Undocumented magic values
"type": schema.StringAttribute{
Required: true,
}
// Users have to guess valid values
// Good: Documented enum
"type": schema.StringAttribute{
Required: true,
MarkdownDescription: "Instance type. Valid values: 't2.micro', 't2.small', 't2.medium', 't2.large'.",
Validators: []validator.String{
stringvalidator.OneOf("t2.micro", "t2.small", "t2.medium", "t2.large"),
},
},
❌ Anti-Pattern 3: Boolean Soup
// Bad: Too many related booleans
"enable_feature_a": schema.BoolAttribute{...}
"enable_feature_b": schema.BoolAttribute{...}
"enable_feature_c": schema.BoolAttribute{...}
"disable_feature_d": schema.BoolAttribute{...}
// Good: Structured configuration
"features": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"a_enabled": schema.BoolAttribute{...},
"b_enabled": schema.BoolAttribute{...},
"c_enabled": schema.BoolAttribute{...},
"d_enabled": schema.BoolAttribute{...},
},
},
❌ Anti-Pattern 4: Overly Generic Names
// Bad: Generic names
"config": schema.MapAttribute{...}
"data": schema.StringAttribute{...}
"value": schema.StringAttribute{...}
// Good: Specific names
"network_config": schema.SingleNestedAttribute{...}
"user_data": schema.StringAttribute{...}
"connection_string": schema.StringAttribute{...}
❌ Anti-Pattern 5: Kitchen Sink Resources
// Bad: One resource does everything
resource "mycloud_everything" "example" {
# 50+ attributes for different things
database_config {...}
network_config {...}
compute_config {...}
storage_config {...}
# ... etc
}
// Good: Separate resources
resource "mycloud_database" "example" {...}
resource "mycloud_network" "example" {...}
resource "mycloud_compute" "example" {...}
resource "mycloud_storage" "example" {...}
❌ Anti-Pattern 6: Not Using Computed
// Bad: User must provide computed values
"id": schema.StringAttribute{
Optional: true, // Wrong! Server generates IDs
}
// Good: Marked as computed
"id": schema.StringAttribute{
Computed: true,
}
Resource vs Data Source Schemas
Resource Schema
// Resource: User provides input, provider manages lifecycle
func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true, // Generated
},
"name": schema.StringAttribute{
Required: true, // User input
},
"status": schema.StringAttribute{
Computed: true, // From API
},
},
}
}
Data Source Schema
// Data source: User provides query, provider returns data
func (d *DatabaseDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Optional: true, // Query by ID
},
"name": schema.StringAttribute{
Optional: true, // Or query by name
},
"status": schema.StringAttribute{
Computed: true, // Returned data
},
},
}
}
Error Handling
Validation Errors
// ✅ Good: Clear, actionable error
resp.Diagnostics.AddAttributeError(
path.Root("port"),
"Invalid Port Number",
fmt.Sprintf("Port must be between 1 and 65535, got: %d", port),
)
// ❌ Bad: Vague error
resp.Diagnostics.AddError("Error", "Invalid port")
API Errors
// ✅ Good: Includes context
resp.Diagnostics.AddError(
"Failed to Create Database",
fmt.Sprintf("API returned error: %s. Database name: %s", err.Error(), name),
)
// ❌ Bad: No context
resp.Diagnostics.AddError("Error", err.Error())
Testing Considerations
Design for Testability
// ✅ Good: Predictable defaults
"region": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("us-east-1"),
}
// Test knows what to expect
resource.TestCheckResourceAttr("mycloud_app.test", "region", "us-east-1")
Avoid Random Defaults
// ❌ Bad: Random name makes testing hard
func (r *MyResource) Create(...) {
if plan.Name.IsNull() {
plan.Name = types.StringValue(fmt.Sprintf("auto-%s", uuid.New()))
}
}
// ✅ Good: Predictable or user-specified
"name": schema.StringAttribute{
Required: true, // User must provide
},
Performance Considerations
Minimize API Calls
// ✅ Good: Single API call with all data
func (r *MyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
resource, err := r.client.CreateResource(ctx, CreateRequest{
Name: plan.Name.ValueString(),
Region: plan.Region.ValueString(),
Config: plan.Config,
// All data in one call
})
}
// ❌ Bad: Multiple API calls
func (r *MyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Create resource
resource, _ := r.client.CreateResource(...)
// Update region
r.client.UpdateRegion(...)
// Update config
r.client.UpdateConfig(...)
}
Cache Client Configuration
// ✅ Good: Configure client once in provider
type MyCloudProvider struct {
client *api.Client // Reused across resources
}
// ❌ Bad: Create client in every resource operation
func (r *MyResource) Create(...) {
client := api.NewClient(...) // Inefficient
}
Schema Evolution Strategy
Version 1.0
// Initial release
"name": schema.StringAttribute{
Required: true,
}
Version 1.1 (Add Optional)
// Safe: Add optional attribute
"name": schema.StringAttribute{
Required: true,
}
"description": schema.StringAttribute{
Optional: true, // ✅ Safe addition
}
Version 2.0 (Deprecate)
// Deprecate old, introduce new
"instance_type": schema.StringAttribute{
Optional: true,
DeprecationMessage: "Use 'instance_class' instead",
}
"instance_class": schema.StringAttribute{
Optional: true,
}
Version 3.0 (Remove Deprecated)
// Remove deprecated attribute (major version)
"instance_class": schema.StringAttribute{
Optional: true,
}
// instance_type removed in v3.0
Checklist
Schema Design
- ✅ Use snake_case naming
- ✅ Descriptive attribute names
- ✅ Minimize required attributes
- ✅ Provide sensible defaults
- ✅ Group related attributes in nested blocks
- ✅ Limit nesting depth to 2-3 levels
Validation
- ✅ Validate in schema, not Create/Update
- ✅ Clear, actionable error messages
- ✅ Document validation rules
- ✅ Validate cross-attribute constraints
Documentation
- ✅ MarkdownDescription for all attributes
- ✅ Document computed attributes
- ✅ Document sensitive attributes
- ✅ Document defaults
- ✅ Document valid values for enums
Backwards Compatibility
- ✅ Never remove attributes
- ✅ Deprecate instead of remove
- ✅ Don’t change attribute types
- ✅ Don’t change optional to required
- ✅ Adding optional attributes is safe
Security
- ✅ Mark sensitive attributes
- ✅ Don’t log sensitive values
- ✅ Use remote encrypted state
- ✅ Document state security implications
Testing
- ✅ Test all validation rules
- ✅ Test default values
- ✅ Test backwards compatibility
- ✅ Test error messages
Key Takeaways
- ✅ Design schemas for intuitive user experience
- ✅ Use consistent naming conventions
- ✅ Minimize required attributes
- ✅ Validate early and comprehensively
- ✅ Maintain backwards compatibility
- ✅ Document everything clearly
- ✅ Avoid common anti-patterns
- ✅ Design for testability
- ✅ Consider performance implications
What’s Next?
Now let’s apply these principles in a hands-on lab.
Previous: ← Nested Schemas
Last updated: June 30, 2026