Data Source Lists
Single vs. List Data Sources
There are two common patterns for data sources:
Single Result Data Sources:
- Return exactly one resource
- Error if zero or multiple matches
- Use singular names (
mycloud_database)
List Data Sources:
- Return zero or more resources
- Provide lists of IDs or detailed objects
- Use plural names (
mycloud_databases)
List Data Source Schema
Simple ID List
type AvailabilityZonesDataSourceModel struct {
Names types.List `tfsdk:"names"`
}
func (d *AvailabilityZonesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches list of availability zones",
Attributes: map[string]schema.Attribute{
"names": schema.ListAttribute{
Computed: true,
ElementType: types.StringType,
MarkdownDescription: "List of availability zone names",
},
},
}
}
Usage:
data "mycloud_availability_zones" "available" {}
output "zones" {
value = data.mycloud_availability_zones.available.names
}
resource "mycloud_subnet" "multi_az" {
for_each = toset(data.mycloud_availability_zones.available.names)
availability_zone = each.value
cidr_block = cidrsubnet("10.0.0.0/16", 8, index(data.mycloud_availability_zones.available.names, each.value))
}
Detailed Object List
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{
MarkdownDescription: "Fetches list of compute instances",
Attributes: map[string]schema.Attribute{
"ids": schema.ListAttribute{
Computed: true,
ElementType: types.StringType,
MarkdownDescription: "List of instance IDs",
},
"instances": schema.ListNestedAttribute{
Computed: true,
MarkdownDescription: "Detailed instance information",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Instance ID",
},
"name": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Instance name",
},
"instance_type": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Instance type",
},
"state": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Current state",
},
"public_ip": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Public IP address",
},
"private_ip": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Private IP address",
},
"tags": schema.MapAttribute{
Computed: true,
ElementType: types.StringType,
MarkdownDescription: "Instance tags",
},
},
},
},
},
Blocks: map[string]schema.Block{
"filter": schema.ListNestedBlock{
MarkdownDescription: "Filter instances",
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"values": schema.ListAttribute{
Required: true,
ElementType: types.StringType,
},
},
},
},
},
}
}
Usage:
data "mycloud_instances" "web_servers" {
filter {
name = "tag:Role"
values = ["web"]
}
}
output "instance_ids" {
value = data.mycloud_instances.web_servers.ids
}
output "instance_details" {
value = data.mycloud_instances.web_servers.instances
}
# Use in resources
resource "mycloud_load_balancer_target" "web" {
for_each = toset(data.mycloud_instances.web_servers.ids)
instance_id = each.value
port = 80
}
Implementing List Data Sources
Simple ID List
func (d *AvailabilityZonesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config AvailabilityZonesDataSourceModel
req.Config.Get(ctx, &config)
// Query API
zones, err := d.client.ListAvailabilityZones(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
tflog.Debug(ctx, "Retrieved availability zones", map[string]any{
"count": len(zones),
})
// Convert to list of strings
names := make([]attr.Value, len(zones))
for i, zone := range zones {
names[i] = types.StringValue(zone.Name)
}
listValue, diags := types.ListValue(types.StringType, names)
resp.Diagnostics.Append(diags...)
config.Names = listValue
// Save state
resp.State.Set(ctx, config)
tflog.Info(ctx, "Successfully read availability zones", map[string]any{
"count": len(zones),
})
}
Detailed Object List
func (d *InstancesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config InstancesDataSourceModel
req.Config.Get(ctx, &config)
// Apply filters
filters := parseFilters(config.Filters)
// Query API
instances, err := d.client.ListInstances(ctx, filters)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
tflog.Debug(ctx, "Retrieved instances", map[string]any{
"count": len(instances),
})
// Build ID list
ids := make([]attr.Value, len(instances))
for i, instance := range instances {
ids[i] = types.StringValue(instance.ID)
}
idsList, diags := types.ListValue(types.StringType, ids)
resp.Diagnostics.Append(diags...)
config.IDs = idsList
// Build detailed list
instanceObjectType := types.ObjectType{
AttrTypes: map[string]attr.Type{
"id": types.StringType,
"name": types.StringType,
"instance_type": types.StringType,
"state": types.StringType,
"public_ip": types.StringType,
"private_ip": types.StringType,
"tags": types.MapType{ElemType: types.StringType},
},
}
instanceObjects := make([]attr.Value, len(instances))
for i, instance := range instances {
// Convert tags
tagsMap := make(map[string]attr.Value)
for k, v := range instance.Tags {
tagsMap[k] = types.StringValue(v)
}
tagsValue, _ := types.MapValue(types.StringType, tagsMap)
// Create object
objValue, diags := types.ObjectValue(
instanceObjectType.AttrTypes,
map[string]attr.Value{
"id": types.StringValue(instance.ID),
"name": types.StringValue(instance.Name),
"instance_type": types.StringValue(instance.Type),
"state": types.StringValue(instance.State),
"public_ip": types.StringValue(instance.PublicIP),
"private_ip": types.StringValue(instance.PrivateIP),
"tags": tagsValue,
},
)
resp.Diagnostics.Append(diags...)
instanceObjects[i] = objValue
}
instancesList, diags := types.ListValue(instanceObjectType, instanceObjects)
resp.Diagnostics.Append(diags...)
config.Instances = instancesList
// Save state
resp.State.Set(ctx, config)
tflog.Info(ctx, "Successfully read instances", map[string]any{
"count": len(instances),
})
}
Handling Empty Results
Option 1: Return Empty List (Recommended)
func (d *DatabasesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config DatabasesDataSourceModel
req.Config.Get(ctx, &config)
// Query databases
dbs, err := d.client.ListDatabases(ctx, filters)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
// Empty list is valid - no error
if len(dbs) == 0 {
tflog.Info(ctx, "No databases found matching criteria")
config.IDs = types.ListNull(types.StringType)
resp.State.Set(ctx, config)
return
}
// Convert to list
ids := make([]attr.Value, len(dbs))
for i, db := range dbs {
ids[i] = types.StringValue(db.ID)
}
listValue, _ := types.ListValue(types.StringType, ids)
config.IDs = listValue
resp.State.Set(ctx, config)
}
Usage:
data "mycloud_databases" "filtered" {
filter {
name = "tag:Environment"
values = ["staging"]
}
}
# Use count to handle empty lists
resource "mycloud_backup" "db_backups" {
count = length(data.mycloud_databases.filtered.ids)
database_id = data.mycloud_databases.filtered.ids[count.index]
}
Option 2: Warning for Empty Results
if len(dbs) == 0 {
resp.Diagnostics.AddWarning(
"No Resources Found",
"No databases found matching the specified criteria. "+
"The data source will return an empty list.",
)
config.IDs = types.ListNull(types.StringType)
resp.State.Set(ctx, config)
return
}
Option 3: Error on Empty (Use Sparingly)
// Only if empty result is truly an error condition
if len(dbs) == 0 {
resp.Diagnostics.AddError(
"No Databases Found",
"Expected at least one database matching criteria, found none.",
)
return
}
Pagination
Schema
type InstancesDataSourceModel struct {
IDs types.List `tfsdk:"ids"`
MaxItems types.Int64 `tfsdk:"max_items"`
NextToken types.String `tfsdk:"next_token"`
}
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,
},
"max_items": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "Maximum number of items to return. Defaults to all",
},
"next_token": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Token for fetching next page of results",
},
},
}
}
Implementation
func (d *InstancesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config InstancesDataSourceModel
req.Config.Get(ctx, &config)
maxItems := 100 // Default
if !config.MaxItems.IsNull() {
maxItems = int(config.MaxItems.ValueInt64())
}
// Query with pagination
result, err := d.client.ListInstances(ctx, &ListOptions{
MaxItems: maxItems,
})
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
// Convert IDs
ids := make([]attr.Value, len(result.Instances))
for i, instance := range result.Instances {
ids[i] = types.StringValue(instance.ID)
}
listValue, _ := types.ListValue(types.StringType, ids)
config.IDs = listValue
// Set next token if more results available
if result.NextToken != "" {
config.NextToken = types.StringValue(result.NextToken)
} else {
config.NextToken = types.StringNull()
}
resp.State.Set(ctx, config)
}
Usage:
# Get first 50 instances
data "mycloud_instances" "page1" {
max_items = 50
filter {
name = "instance-state-name"
values = ["running"]
}
}
output "next_page_token" {
value = data.mycloud_instances.page1.next_token
}
Sorting Lists
"sort_by": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Field to sort by (name, created_at, etc.)",
},
"sort_order": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Sort order: asc or desc. Defaults to asc",
},
func (d *DatabasesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config DatabasesDataSourceModel
req.Config.Get(ctx, &config)
// Query databases
dbs, err := d.client.ListDatabases(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
// Sort if requested
if !config.SortBy.IsNull() {
sortBy := config.SortBy.ValueString()
sortOrder := "asc"
if !config.SortOrder.IsNull() {
sortOrder = config.SortOrder.ValueString()
}
sortDatabases(dbs, sortBy, sortOrder)
tflog.Debug(ctx, "Sorted databases", map[string]any{
"sort_by": sortBy,
"sort_order": sortOrder,
})
}
// Convert to list...
}
func sortDatabases(dbs []Database, sortBy, order string) {
sort.Slice(dbs, func(i, j int) bool {
var less bool
switch sortBy {
case "name":
less = dbs[i].Name < dbs[j].Name
case "created_at":
less = dbs[i].CreatedAt < dbs[j].CreatedAt
case "storage_gb":
less = dbs[i].StorageGB < dbs[j].StorageGB
default:
less = dbs[i].Name < dbs[j].Name
}
if order == "desc" {
return !less
}
return less
})
}
Usage:
data "mycloud_databases" "sorted" {
sort_by = "created_at"
sort_order = "desc"
filter {
name = "engine"
values = ["postgres"]
}
}
output "newest_database" {
value = data.mycloud_databases.sorted.ids[0]
}
Limiting Results
"limit": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "Maximum number of results to return",
},
func (d *InstancesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config InstancesDataSourceModel
req.Config.Get(ctx, &config)
// Query all matching instances
instances, err := d.client.ListInstances(ctx, filters)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
tflog.Debug(ctx, "Retrieved instances", map[string]any{
"total": len(instances),
})
// Apply limit if specified
if !config.Limit.IsNull() {
limit := int(config.Limit.ValueInt64())
if limit < len(instances) {
instances = instances[:limit]
tflog.Debug(ctx, "Applied limit", map[string]any{
"limit": limit,
"result": len(instances),
})
}
}
// Convert to list...
}
Usage:
data "mycloud_instances" "sample" {
limit = 10
filter {
name = "instance-state-name"
values = ["running"]
}
}
Combined Example
Complete list data source with filtering, sorting, and limiting:
type DatabasesDataSourceModel struct {
IDs types.List `tfsdk:"ids"`
Databases types.List `tfsdk:"databases"`
Filters []FilterModel `tfsdk:"filter"`
SortBy types.String `tfsdk:"sort_by"`
SortOrder types.String `tfsdk:"sort_order"`
Limit types.Int64 `tfsdk:"limit"`
}
func (d *DatabasesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config DatabasesDataSourceModel
req.Config.Get(ctx, &config)
tflog.Debug(ctx, "Reading databases data source")
// 1. Query with filters
filters := parseFilters(config.Filters)
dbs, err := d.client.ListDatabases(ctx, filters)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
tflog.Debug(ctx, "Retrieved databases", map[string]any{
"count": len(dbs),
})
// 2. Sort if requested
if !config.SortBy.IsNull() {
sortBy := config.SortBy.ValueString()
sortOrder := "asc"
if !config.SortOrder.IsNull() {
sortOrder = config.SortOrder.ValueString()
}
sortDatabases(dbs, sortBy, sortOrder)
}
// 3. Apply limit if specified
if !config.Limit.IsNull() {
limit := int(config.Limit.ValueInt64())
if limit > 0 && limit < len(dbs) {
dbs = dbs[:limit]
}
}
// 4. Handle empty results
if len(dbs) == 0 {
config.IDs = types.ListNull(types.StringType)
config.Databases = types.ListNull(databaseObjectType)
resp.State.Set(ctx, config)
return
}
// 5. Build ID list
ids := make([]attr.Value, len(dbs))
for i, db := range dbs {
ids[i] = types.StringValue(db.ID)
}
listValue, _ := types.ListValue(types.StringType, ids)
config.IDs = listValue
// 6. Build detailed list
databaseObjectType := types.ObjectType{
AttrTypes: map[string]attr.Type{
"id": types.StringType,
"name": types.StringType,
"engine": types.StringType,
"endpoint": types.StringType,
"status": types.StringType,
},
}
dbObjects := make([]attr.Value, len(dbs))
for i, db := range dbs {
objValue, _ := types.ObjectValue(
databaseObjectType.AttrTypes,
map[string]attr.Value{
"id": types.StringValue(db.ID),
"name": types.StringValue(db.Name),
"engine": types.StringValue(db.Engine),
"endpoint": types.StringValue(db.Endpoint),
"status": types.StringValue(db.Status),
},
)
dbObjects[i] = objValue
}
dbList, _ := types.ListValue(databaseObjectType, dbObjects)
config.Databases = dbList
// 7. Save state
resp.State.Set(ctx, config)
tflog.Info(ctx, "Successfully read databases", map[string]any{
"count": len(dbs),
})
}
Usage:
data "mycloud_databases" "production" {
filter {
name = "tag:Environment"
values = ["production"]
}
filter {
name = "status"
values = ["available"]
}
sort_by = "created_at"
sort_order = "desc"
limit = 5
}
output "database_ids" {
value = data.mycloud_databases.production.ids
}
output "database_details" {
value = data.mycloud_databases.production.databases
}
# Create backups for each
resource "mycloud_backup" "production_dbs" {
for_each = toset(data.mycloud_databases.production.ids)
database_id = each.value
schedule = "daily"
}
Best Practices
1. Provide Both IDs and Details
// ✅ Good: Offer both simple IDs and detailed objects
"ids": schema.ListAttribute{
Computed: true,
ElementType: types.StringType,
},
"instances": schema.ListNestedAttribute{
Computed: true,
NestedObject: schema.NestedAttributeObject{
// Detailed attributes...
},
},
2. Return Empty Lists, Not Errors
// ✅ Good: Empty list is valid
if len(results) == 0 {
config.IDs = types.ListNull(types.StringType)
resp.State.Set(ctx, config)
return
}
// ❌ Bad: Error on empty (unless truly exceptional)
if len(results) == 0 {
resp.Diagnostics.AddError("No Results", "...")
return
}
3. Log List Sizes
tflog.Debug(ctx, "Query results", map[string]any{
"total": len(allResults),
"filtered": len(filtered),
"after_sort": len(sorted),
"final": len(final),
})
4. Document List Behavior
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches databases matching criteria.\n\n" +
"Returns an empty list if no databases match. " +
"Use filtering to narrow results.",
}
Key Takeaways
- ✅ Use plural names for list data sources
- ✅ Provide both simple ID lists and detailed object lists
- ✅ Return empty lists when no results found (don’t error)
- ✅ Support filtering to narrow results
- ✅ Offer sorting options when useful
- ✅ Implement limiting to control result size
- ✅ Consider pagination for very large result sets
- ✅ Log query and filtering operations
- ✅ Handle empty results gracefully
- ✅ Use
for_eachandcountin examples
What’s Next?
Put it all together in a comprehensive hands-on lab!
Previous: ← Data Source Filtering
Last updated: June 30, 2026