Data Source Lists

9 minute read

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

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_each and count in examples

What’s Next?

Put it all together in a comprehensive hands-on lab!

Next: Data Sources Lab →


Previous: ← Data Source Filtering

Last updated: June 30, 2026

Updated: