Required, Optional, and Computed

7 minute read

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

Updated: