Schema Best Practices

8 minute read

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

// ✅ 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.

Next: Schema Design Lab →


Previous: ← Nested Schemas

Last updated: June 30, 2026

Updated: