Data Source Schema

8 minute read

Schema Design Principles

Data source schemas differ from resource schemas in important ways:

Key differences:

  • Most attributes are Computed (results from query)
  • Some attributes are Required or Optional (search criteria)
  • No Create, Update, or Delete operations
  • Focus on query flexibility
  • Filter blocks for complex searches

Required vs. Optional vs. Computed

Required Attributes

Attributes the user must provide to identify the resource:

"name": schema.StringAttribute{
    Required:            true,
    MarkdownDescription: "Name of the database to look up",
},

Usage:

data "mycloud_database" "app" {
  name = "production-db"  # Required
}

Optional Attributes

Additional search criteria or configuration:

"region": schema.StringAttribute{
    Optional:            true,
    MarkdownDescription: "Region to search in. Defaults to provider region",
},

"tags": schema.MapAttribute{
    Optional:            true,
    ElementType:         types.StringType,
    MarkdownDescription: "Tags to filter by",
},

Usage:

data "mycloud_database" "app" {
  name   = "production-db"
  region = "us-west-2"  # Optional refinement
  
  tags = {              # Optional filter
    Environment = "production"
  }
}

Computed Attributes

Results returned from the query:

"id": schema.StringAttribute{
    Computed:            true,
    MarkdownDescription: "Database identifier",
},

"endpoint": schema.StringAttribute{
    Computed:            true,
    MarkdownDescription: "Connection endpoint",
},

"port": schema.Int64Attribute{
    Computed:            true,
    MarkdownDescription: "Connection port",
},

All query results should be Computed.

Complete Schema Example

func (d *DatabaseDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
    resp.Schema = schema.Schema{
        MarkdownDescription: "Fetches information about a CacheCloud database instance",
        
        Attributes: map[string]schema.Attribute{
            // Search criteria (Required/Optional)
            "id": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "Database ID. Conflicts with `name`",
            },
            "name": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "Database name. Conflicts with `id`",
            },
            "tags": schema.MapAttribute{
                Optional:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "Tags to filter by. All tags must match",
            },
            
            // Query results (Computed)
            "arn": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Amazon Resource Name (ARN) of the database",
            },
            "engine": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Database engine (postgres, mysql, etc.)",
            },
            "engine_version": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Database engine version",
            },
            "endpoint": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Connection endpoint",
            },
            "port": schema.Int64Attribute{
                Computed:            true,
                MarkdownDescription: "Connection port",
            },
            "status": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Current status",
            },
            "instance_class": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Instance class",
            },
            "storage_gb": schema.Int64Attribute{
                Computed:            true,
                MarkdownDescription: "Allocated storage in GB",
            },
            "multi_az": schema.BoolAttribute{
                Computed:            true,
                MarkdownDescription: "Whether Multi-AZ is enabled",
            },
            "created_at": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Creation timestamp",
            },
            "security_group_ids": schema.ListAttribute{
                Computed:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "Security group IDs",
            },
            "subnet_ids": schema.ListAttribute{
                Computed:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "Subnet IDs",
            },
        },
    }
}

Filter Blocks

For complex filtering, use nested filter blocks:

func (d *InstancesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
    resp.Schema = schema.Schema{
        MarkdownDescription: "Fetches a list of compute instances",
        
        Attributes: map[string]schema.Attribute{
            "ids": schema.ListAttribute{
                Computed:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "List of instance IDs matching filters",
            },
        },
        
        Blocks: map[string]schema.Block{
            "filter": schema.ListNestedBlock{
                MarkdownDescription: "Filter instances. Multiple filters are ANDed together",
                
                NestedObject: schema.NestedBlockObject{
                    Attributes: map[string]schema.Attribute{
                        "name": schema.StringAttribute{
                            Required:            true,
                            MarkdownDescription: "Name of the filter field",
                        },
                        "values": schema.ListAttribute{
                            Required:            true,
                            ElementType:         types.StringType,
                            MarkdownDescription: "Filter values (OR within filter)",
                        },
                    },
                },
            },
        },
    }
}

Usage:

data "mycloud_instances" "web_servers" {
  filter {
    name   = "tag:Role"
    values = ["web", "frontend"]  # Match either
  }
  
  filter {
    name   = "tag:Environment"
    values = ["production"]
  }
  
  filter {
    name   = "instance-state-name"
    values = ["running"]
  }
}

Model:

type InstancesDataSourceModel struct {
    IDs     types.List   `tfsdk:"ids"`
    Filters []FilterModel `tfsdk:"filter"`
}

type FilterModel struct {
    Name   types.String `tfsdk:"name"`
    Values types.List   `tfsdk:"values"`
}

Validation

Built-in Validators

import (
    "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
    "github.com/hashicorp/terraform-plugin-framework/path"
)

"id": schema.StringAttribute{
    Optional: true,
    MarkdownDescription: "Database ID",
    Validators: []validator.String{
        // ID and name are mutually exclusive
        stringvalidator.ConflictsWith(path.Expressions{
            path.MatchRoot("name"),
        }...),
        // ID must match pattern
        stringvalidator.RegexMatches(
            regexp.MustCompile(`^db-[a-z0-9]+$`),
            "must be a valid database ID (db-xxxxx)",
        ),
    },
},

"name": schema.StringAttribute{
    Optional: true,
    MarkdownDescription: "Database name",
    Validators: []validator.String{
        // Name and ID are mutually exclusive
        stringvalidator.ConflictsWith(path.Expressions{
            path.MatchRoot("id"),
        }...),
        // Name length constraints
        stringvalidator.LengthBetween(1, 63),
    },
},

AtLeastOneOf

Require at least one identifier:

import (
    "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
)

"id": schema.StringAttribute{
    Optional: true,
    Validators: []validator.String{
        stringvalidator.AtLeastOneOf(
            path.MatchRoot("id"),
            path.MatchRoot("name"),
        ),
    },
},

"name": schema.StringAttribute{
    Optional: true,
    Validators: []validator.String{
        stringvalidator.AtLeastOneOf(
            path.MatchRoot("id"),
            path.MatchRoot("name"),
        ),
    },
},

Custom Validation

Implement DataSourceWithValidateConfig:

func (d *DatabaseDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
    var config DatabaseDataSourceModel
    
    diags := req.Config.Get(ctx, &config)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Check that at least one search criterion is provided
    hasID := !config.ID.IsNull()
    hasName := !config.Name.IsNull()
    hasTags := !config.Tags.IsNull()
    
    if !hasID && !hasName && !hasTags {
        resp.Diagnostics.AddError(
            "Missing Search Criteria",
            "At least one of 'id', 'name', or 'tags' must be specified",
        )
    }
    
    // Check for conflicting criteria
    if hasID && hasName {
        resp.Diagnostics.AddError(
            "Conflicting Search Criteria",
            "Only one of 'id' or 'name' can be specified",
        )
    }
}

Common Schema Patterns

1. Single Lookup (ID or Name)

func (d *DatabaseDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            // One of these required
            "id": schema.StringAttribute{
                Optional: true,
                Validators: []validator.String{
                    stringvalidator.ExactlyOneOf(
                        path.MatchRoot("id"),
                        path.MatchRoot("name"),
                    ),
                },
            },
            "name": schema.StringAttribute{
                Optional: true,
                Validators: []validator.String{
                    stringvalidator.ExactlyOneOf(
                        path.MatchRoot("id"),
                        path.MatchRoot("name"),
                    ),
                },
            },
            
            // Results
            "endpoint": schema.StringAttribute{
                Computed: true,
            },
            "port": schema.Int64Attribute{
                Computed: true,
            },
        },
    }
}

Usage:

# By ID
data "mycloud_database" "by_id" {
  id = "db-12345"
}

# By name
data "mycloud_database" "by_name" {
  name = "production-db"
}

2. Most Recent Pattern

"most_recent": schema.BoolAttribute{
    Optional:            true,
    MarkdownDescription: "If multiple results match, return the most recent",
},

Usage:

data "mycloud_ami" "latest_ubuntu" {
  most_recent = true
  
  filter {
    name   = "name"
    values = ["ubuntu-22.04-*"]
  }
}

3. Tag-Based Lookup

"tags": schema.MapAttribute{
    Optional:            true,
    ElementType:         types.StringType,
    MarkdownDescription: "Tags to match. All specified tags must match exactly",
},

Usage:

data "mycloud_database" "shared" {
  tags = {
    Shared      = "true"
    Environment = "production"
    Team        = "platform"
  }
}

4. List Results

type InstancesDataSourceModel struct {
    IDs       types.List `tfsdk:"ids"`
    Instances types.List `tfsdk:"instances"`
}

func (d *InstancesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "ids": schema.ListAttribute{
                Computed:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "List of instance IDs",
            },
            "instances": schema.ListNestedAttribute{
                Computed:            true,
                MarkdownDescription: "List of instance details",
                NestedObject: schema.NestedAttributeObject{
                    Attributes: map[string]schema.Attribute{
                        "id": schema.StringAttribute{
                            Computed: true,
                        },
                        "name": schema.StringAttribute{
                            Computed: true,
                        },
                        "state": schema.StringAttribute{
                            Computed: true,
                        },
                        "instance_type": schema.StringAttribute{
                            Computed: true,
                        },
                    },
                },
            },
        },
    }
}

Usage:

data "mycloud_instances" "web" {
  filter {
    name   = "tag:Role"
    values = ["web"]
  }
}

output "instance_ids" {
  value = data.mycloud_instances.web.ids
}

output "instance_details" {
  value = data.mycloud_instances.web.instances
}

5. Region/Account Information

func (d *RegionsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "all_regions": schema.BoolAttribute{
                Optional:            true,
                MarkdownDescription: "Include all regions, not just available ones",
            },
            "names": schema.ListAttribute{
                Computed:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "List of region names",
            },
            "regions": schema.ListNestedAttribute{
                Computed: true,
                NestedObject: schema.NestedAttributeObject{
                    Attributes: map[string]schema.Attribute{
                        "name": schema.StringAttribute{
                            Computed: true,
                        },
                        "endpoint": schema.StringAttribute{
                            Computed: true,
                        },
                        "description": schema.StringAttribute{
                            Computed: true,
                        },
                    },
                },
            },
        },
    }
}

Usage:

data "mycloud_regions" "available" {
  all_regions = false
}

resource "mycloud_bucket" "regional" {
  for_each = toset(data.mycloud_regions.available.names)
  
  name   = "backup-${each.value}"
  region = each.value
}

Complex Nested Attributes

Object Attributes

"connection_info": schema.SingleNestedAttribute{
    Computed: true,
    Attributes: map[string]schema.Attribute{
        "endpoint": schema.StringAttribute{
            Computed: true,
        },
        "port": schema.Int64Attribute{
            Computed: true,
        },
        "protocol": schema.StringAttribute{
            Computed: true,
        },
    },
},

Usage:

data "mycloud_database" "app" {
  name = "production-db"
}

output "connection_string" {
  value = "${data.mycloud_database.app.connection_info.endpoint}:${data.mycloud_database.app.connection_info.port}"
}

List of Objects

"replicas": schema.ListNestedAttribute{
    Computed: true,
    NestedObject: schema.NestedAttributeObject{
        Attributes: map[string]schema.Attribute{
            "id": schema.StringAttribute{
                Computed: true,
            },
            "region": schema.StringAttribute{
                Computed: true,
            },
            "status": schema.StringAttribute{
                Computed: true,
            },
            "endpoint": schema.StringAttribute{
                Computed: true,
            },
        },
    },
},

Sensitive Attributes

Mark sensitive data appropriately:

"password": schema.StringAttribute{
    Computed:            true,
    Sensitive:           true,
    MarkdownDescription: "Master password (sensitive)",
},

"connection_string": schema.StringAttribute{
    Computed:            true,
    Sensitive:           true,
    MarkdownDescription: "Full connection string with credentials",
},

Result:

data.mycloud_database.app: Reading...
data.mycloud_database.app: Read complete after 1s

Changes to Outputs:
  + connection_string = (sensitive value)
  + password          = (sensitive value)

Documentation Best Practices

Clear Descriptions

"name": schema.StringAttribute{
    Required: true,
    MarkdownDescription: "Exact name of the database instance to look up. " +
        "The search is case-sensitive and must match the full database name.",
},

"tags": schema.MapAttribute{
    Optional:    true,
    ElementType: types.StringType,
    MarkdownDescription: "Map of tags to filter databases. " +
        "Only databases matching **all** specified tags will be returned. " +
        "Tag keys and values are case-sensitive.",
},

"endpoint": schema.StringAttribute{
    Computed: true,
    MarkdownDescription: "The connection endpoint in the format `hostname:port`. " +
        "Use this to configure application database connections.",
},

Usage Examples in Schema

resp.Schema = schema.Schema{
    MarkdownDescription: "Fetches information about a database instance.\n\n" +
        "## Example Usage\n\n" +
        "```hcl\n" +
        "data \"mycloud_database\" \"app\" {\n" +
        "  name = \"production-db\"\n" +
        "}\n\n" +
        "resource \"mycloud_app\" \"api\" {\n" +
        "  database_endpoint = data.mycloud_database.app.endpoint\n" +
        "}\n" +
        "```",
    
    Attributes: map[string]schema.Attribute{
        // ...
    },
}

Schema Evolution

Deprecating Attributes

"old_name": schema.StringAttribute{
    Optional:            true,
    DeprecationMessage:  "Use 'name' instead. This attribute will be removed in version 2.0",
    MarkdownDescription: "Deprecated: Use `name` instead",
},

"name": schema.StringAttribute{
    Optional:            true,
    MarkdownDescription: "Database name to look up",
},

Adding Optional Attributes

Safe to add without breaking changes:

// v1.0 - Original schema
"name": schema.StringAttribute{
    Required: true,
},

// v1.1 - Add optional filter (non-breaking)
"tags": schema.MapAttribute{
    Optional:    true,  // New optional attribute
    ElementType: types.StringType,
},

Complete Example

func (d *DatabaseDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
    resp.Schema = schema.Schema{
        MarkdownDescription: "Fetches information about a CacheCloud database instance",
        
        Attributes: map[string]schema.Attribute{
            // Search criteria
            "id": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "Database identifier. Conflicts with `name`",
                Validators: []validator.String{
                    stringvalidator.ConflictsWith(path.Expressions{
                        path.MatchRoot("name"),
                    }...),
                    stringvalidator.RegexMatches(
                        regexp.MustCompile(`^db-[a-z0-9]+$`),
                        "must match pattern 'db-xxxxx'",
                    ),
                },
            },
            
            "name": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "Database name. Conflicts with `id`",
                Validators: []validator.String{
                    stringvalidator.ConflictsWith(path.Expressions{
                        path.MatchRoot("id"),
                    }...),
                    stringvalidator.LengthBetween(1, 63),
                },
            },
            
            "tags": schema.MapAttribute{
                Optional:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "Tags to filter by. All must match",
            },
            
            // Query results
            "arn": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Amazon Resource Name",
            },
            
            "engine": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Database engine",
            },
            
            "engine_version": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Engine version",
            },
            
            "endpoint": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Connection endpoint",
            },
            
            "port": schema.Int64Attribute{
                Computed:            true,
                MarkdownDescription: "Connection port",
            },
            
            "status": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Current status",
            },
            
            "instance_class": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Instance class",
            },
            
            "storage_gb": schema.Int64Attribute{
                Computed:            true,
                MarkdownDescription: "Allocated storage in GB",
            },
            
            "multi_az": schema.BoolAttribute{
                Computed:            true,
                MarkdownDescription: "Multi-AZ enabled",
            },
            
            "security_group_ids": schema.ListAttribute{
                Computed:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "Security group IDs",
            },
            
            "subnet_ids": schema.ListAttribute{
                Computed:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "Subnet IDs",
            },
            
            "created_at": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Creation timestamp",
            },
        },
    }
}

Key Takeaways

  • ✅ Search criteria are Required or Optional
  • ✅ Query results are always Computed
  • ✅ Use filter blocks for complex queries
  • ✅ Validate mutually exclusive attributes
  • ✅ Provide clear, detailed descriptions
  • ✅ Use validators for input validation
  • ✅ Mark sensitive data appropriately
  • ✅ Support multiple lookup methods when useful
  • ✅ Document with examples in schema
  • ✅ Consider schema evolution and deprecation

What’s Next?

Now let’s implement the Read operation to query APIs and populate the schema!

Next: Data Source Read →


Previous: ← Data Source Basics

Last updated: June 30, 2026

Updated: