Required, Optional, and Computed
Attribute Configuration Fundamentals
Every attribute in a Terraform provider schema has a configuration that determines:
- Whether users must provide it
- Whether users can provide it
- Whether the provider computes it
Understanding these configurations is essential for designing intuitive schemas.
The Three States
Required
User MUST provide a value
"name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Name of the environment (required)",
},
resource "mycloud_environment" "example" {
name = "production" # Must be specified
}
Optional
User MAY provide a value
"description": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Optional description",
},
resource "mycloud_environment" "example" {
name = "production"
description = "Prod environment" # Can be omitted
}
Computed
Provider WILL provide a value
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Unique identifier",
},
resource "mycloud_environment" "example" {
name = "production"
# id is computed by provider after creation
}
output "env_id" {
value = mycloud_environment.example.id # Available after apply
}
Valid Combinations
Required Only
User must provide, provider doesn’t compute
"name": schema.StringAttribute{
Required: true,
},
Use case: Essential input values
Example:
resource "mycloud_environment" "example" {
name = "production" # Required
}
Optional Only
User may provide, provider doesn’t compute
"description": schema.StringAttribute{
Optional: true,
},
Use case: Nice-to-have fields
Example:
resource "mycloud_environment" "example" {
name = "production"
description = "Production environment" # Optional
}
Computed Only
Provider computes, user cannot set
"id": schema.StringAttribute{
Computed: true,
},
Use case: Server-generated values (IDs, timestamps, ARNs)
Example:
resource "mycloud_environment" "example" {
name = "production"
# id is computed automatically
}
Access computed value:
output "environment_id" {
value = mycloud_environment.example.id
}
Optional + Computed
User may provide, OR provider will compute
"region": schema.StringAttribute{
Optional: true,
Computed: true,
},
Use case: Values with provider defaults
Example:
# User specifies
resource "mycloud_environment" "custom" {
name = "staging"
region = "us-west-2" # User-provided
}
# Provider uses default
resource "mycloud_environment" "default" {
name = "dev"
# region will be computed (e.g., "us-east-1")
}
Implementation:
func (r *EnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan EnvironmentModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
// If region not specified, use default
if plan.Region.IsNull() {
plan.Region = types.StringValue("us-east-1")
}
// API call with region
env, err := r.client.CreateEnvironment(ctx, CreateEnvRequest{
Name: plan.Name.ValueString(),
Region: plan.Region.ValueString(),
})
// Set computed values
plan.ID = types.StringValue(env.ID)
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
Required + Computed (Rare)
User must provide initially, provider may update
"password": schema.StringAttribute{
Required: true,
Computed: true,
Sensitive: true,
},
Use case: Values that can be set by user but may be regenerated
Example:
resource "mycloud_database" "example" {
name = "mydb"
password = "initial-password" # Required on create
# Password may be rotated by provider
}
⚠️ Warning: This pattern is uncommon and can confuse users. Usually better to have separate attributes.
Invalid Combinations
❌ Required + Optional
// INVALID: Cannot be both required and optional
"name": schema.StringAttribute{
Required: true,
Optional: true, // ❌ Compile error
},
❌ No Configuration
// INVALID: Must specify at least one
"name": schema.StringAttribute{
// ❌ Missing Required, Optional, or Computed
},
Default Values
Using Optional + Computed
The Optional + Computed pattern with ModifyPlan is the standard way to provide defaults:
// Schema
"port": schema.NumberAttribute{
Optional: true,
Computed: true,
},
// Implementation
func (r *MyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
var plan MyResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if req.Plan.Raw.IsNull() {
// Resource is being destroyed
return
}
// Set default if not provided
if plan.Port.IsNull() {
plan.Port = types.NumberValue(big.NewFloat(8080))
}
diags = resp.Plan.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
Default Values in Plugin Framework
The framework provides Default modifiers:
"enabled": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
Available defaults:
booldefault.StaticBool(true)stringdefault.StaticString("default")numberdefault.StaticNumber(big.NewFloat(0))listdefault.StaticList(...)mapdefault.StaticMap(...)setdefault.StaticSet(...)
Common Patterns
Pattern 1: Resource Identity
// ID - Always computed
"id": schema.StringAttribute{
Computed: true,
},
// Name - Usually required
"name": schema.StringAttribute{
Required: true,
},
// ARN - Always computed
"arn": schema.StringAttribute{
Computed: true,
},
Pattern 2: Timestamps
"created_at": schema.StringAttribute{
Computed: true,
},
"updated_at": schema.StringAttribute{
Computed: true,
},
Pattern 3: Optional Configuration
"timeout": schema.NumberAttribute{
Optional: true,
Computed: true,
Default: numberdefault.StaticNumber(big.NewFloat(30)),
},
"retries": schema.NumberAttribute{
Optional: true,
Computed: true,
Default: numberdefault.StaticNumber(big.NewFloat(3)),
},
Pattern 4: Feature Flags
"auto_scaling": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"monitoring": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
Pattern 5: Server-Assigned Values
"status": schema.StringAttribute{
Computed: true, // Server determines status
},
"endpoint": schema.StringAttribute{
Computed: true, // Server assigns endpoint
},
Null vs Empty vs Zero
Understanding the difference is critical for Optional + Computed:
// String
types.StringNull() // Not provided by user
types.StringValue("") // User provided empty string
types.StringValue("x") // User provided value
// Number
types.NumberNull() // Not provided
types.NumberValue(big.NewFloat(0)) // User provided 0
// Bool
types.BoolNull() // Not provided
types.BoolValue(false) // User explicitly set false
// List
types.ListNull(types.StringType) // Not provided
types.ListValue(types.StringType, []attr.Value{}) // User provided []
Why it matters:
func (r *MyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan MyResourceModel
req.Plan.Get(ctx, &plan)
// Check if user provided tags
if plan.Tags.IsNull() {
// User didn't specify tags - use default
plan.Tags, _ = types.ListValue(types.StringType, []attr.Value{
types.StringValue("default"),
})
} else if len(plan.Tags.Elements()) == 0 {
// User specified tags = [] - respect their choice
// Don't add defaults
}
resp.State.Set(ctx, plan)
}
Computed Attribute Updates
Read Operations
Computed attributes should be updated in Read:
func (r *EnvironmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state EnvironmentModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
// Fetch current state from API
env, err := r.client.GetEnvironment(ctx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Read Error", err.Error())
return
}
// Update computed values
state.Status = types.StringValue(env.Status)
state.Endpoint = types.StringValue(env.Endpoint)
state.UpdatedAt = types.StringValue(env.UpdatedAt)
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
}
Create Operations
Computed attributes are set during Create:
func (r *EnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan EnvironmentModel
req.Plan.Get(ctx, &plan)
// Create resource via API
env, err := r.client.CreateEnvironment(ctx, ...)
// Set computed attributes from API response
plan.ID = types.StringValue(env.ID)
plan.ARN = types.StringValue(env.ARN)
plan.CreatedAt = types.StringValue(env.CreatedAt)
plan.Status = types.StringValue(env.Status)
resp.State.Set(ctx, plan)
}
State Management
Required Attributes
Always in state (user must provide):
# State file
"name": "production" # Always present
Optional Attributes
May be null in state:
# State file
"description": null # If not provided
# or
"description": "Prod env" # If provided
Computed Attributes
Always populated after create/read:
# State file after apply
"id": "env-abc123"
"arn": "arn:aws:..."
"created_at": "2024-01-15T10:30:00Z"
Plan vs State
Plan (Before Apply)
# What user configured
name = "production" # Required (present)
description = null # Optional (not provided)
id = null # Computed (not yet known)
State (After Apply)
# What actually exists
name = "production" # Required (from user)
description = null # Optional (still not set)
id = "env-abc123" # Computed (from provider)
Validation Considerations
Required Attributes
No null check needed—Terraform validates:
// No need for null check
func (r *MyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan MyResourceModel
req.Plan.Get(ctx, &plan)
// name is guaranteed to be non-null
name := plan.Name.ValueString()
}
Optional Attributes
Always check for null:
// Check if optional field provided
if !plan.Description.IsNull() {
description := plan.Description.ValueString()
// Use description
}
Computed Attributes
Don’t read from plan, set in create/read:
// ❌ Wrong: Don't read computed from plan
id := plan.ID.ValueString() // Will be null!
// ✅ Right: Set computed during create
env, _ := r.client.CreateEnvironment(...)
plan.ID = types.StringValue(env.ID)
Best Practices
1. Choose Semantic Configuration
// ✅ Good: Name is essential
"name": schema.StringAttribute{
Required: true,
},
// ✅ Good: ID is server-generated
"id": schema.StringAttribute{
Computed: true,
},
// ✅ Good: Region has default
"region": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("us-east-1"),
},
2. Document Defaults
"port": schema.NumberAttribute{
Optional: true,
Computed: true,
MarkdownDescription: "Port number (default: 8080)",
Default: numberdefault.StaticNumber(big.NewFloat(8080)),
},
3. Minimize Required Fields
// ✅ Good: Only truly essential fields required
"name": schema.StringAttribute{
Required: true, // Can't create without name
},
// ❌ Bad: Making optional fields required
"description": schema.StringAttribute{
Required: true, // Not essential—should be optional
},
4. Use Computed for Server Values
// ✅ Good: Values from server are computed
"created_at": schema.StringAttribute{
Computed: true,
},
"last_modified": schema.StringAttribute{
Computed: true,
},
5. Provide Sensible Defaults
// ✅ Good: Defaults for common use cases
"timeout": schema.NumberAttribute{
Optional: true,
Computed: true,
Default: numberdefault.StaticNumber(big.NewFloat(30)),
},
"enabled": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
Common Mistakes
❌ Mistake 1: Making Everything Required
// Bad: Too restrictive
"name": schema.StringAttribute{
Required: true,
},
"description": schema.StringAttribute{
Required: true, // Should be optional
},
"labels": schema.MapAttribute{
Required: true, // Should be optional
},
❌ Mistake 2: Not Using Computed for IDs
// Bad: ID should be computed
"id": schema.StringAttribute{
Optional: true, // Wrong! Server generates IDs
},
// Good: ID is computed
"id": schema.StringAttribute{
Computed: true,
},
❌ Mistake 3: No Defaults for Optional Fields
// Bad: No default for timeout
"timeout": schema.NumberAttribute{
Optional: true, // What happens if not set?
},
// Good: Has default
"timeout": schema.NumberAttribute{
Optional: true,
Computed: true,
Default: numberdefault.StaticNumber(big.NewFloat(30)),
},
Key Takeaways
- ✅ Required: User must provide
- ✅ Optional: User may provide
- ✅ Computed: Provider will provide
- ✅ Optional + Computed: User may provide OR provider defaults
- ✅ Use defaults for optional configuration
- ✅ All IDs and server-generated values should be Computed
- ✅ Minimize required fields for better UX
- ✅ Always check if optional values are null
- ✅ Document default values in descriptions
What’s Next?
Now let’s learn about validation and constraints.
Next: Validation and Constraints →
Previous: ← Blocks vs Attributes
Last updated: June 30, 2026