Data Source Schema
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!
Previous: ← Data Source Basics
Last updated: June 30, 2026