Nested Schemas
Understanding Nested Schemas
Nested schemas allow you to model complex, hierarchical configurations. They’re essential for resources with rich configuration structures.
In this chapter, you’ll learn how to create deeply nested schemas that are both powerful and user-friendly.
Types of Nested Attributes
1. SingleNestedAttribute
One occurrence only
2. ListNestedAttribute
Multiple ordered occurrences
3. SetNestedAttribute
Multiple unordered occurrences
4. MapNestedAttribute
Key-value nested structures
SingleNestedAttribute
Use for one-to-one relationships.
Schema Definition
"network": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"vpc_id": schema.StringAttribute{
Required: true,
},
"subnet_id": schema.StringAttribute{
Required: true,
},
"security_group_ids": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
},
},
},
HCL Usage
resource "mycloud_app" "example" {
name = "myapp"
network {
vpc_id = "vpc-123"
subnet_id = "subnet-456"
security_group_ids = ["sg-789", "sg-abc"]
}
}
Model Definition
type AppModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Network *NetworkModel `tfsdk:"network"`
}
type NetworkModel struct {
VpcID types.String `tfsdk:"vpc_id"`
SubnetID types.String `tfsdk:"subnet_id"`
SecurityGroupIDs types.List `tfsdk:"security_group_ids"`
}
Working with SingleNestedAttribute
func (r *AppResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan AppModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
// Check if network block provided
if plan.Network != nil {
// Access nested fields
vpcID := plan.Network.VpcID.ValueString()
subnetID := plan.Network.SubnetID.ValueString()
// Extract security group IDs
var sgIDs []string
if !plan.Network.SecurityGroupIDs.IsNull() {
diags := plan.Network.SecurityGroupIDs.ElementsAs(ctx, &sgIDs, false)
resp.Diagnostics.Append(diags...)
}
// Create with network config
app, err := r.client.CreateApp(ctx, CreateAppRequest{
Name: plan.Name.ValueString(),
VpcID: vpcID,
SubnetID: subnetID,
SecurityGroupIDs: sgIDs,
})
} else {
// Create without network config
app, err := r.client.CreateApp(ctx, CreateAppRequest{
Name: plan.Name.ValueString(),
})
}
// ... handle response ...
}
ListNestedAttribute
Use for one-to-many ordered relationships.
Schema Definition
"rule": schema.ListNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"port": schema.NumberAttribute{
Required: true,
},
"protocol": schema.StringAttribute{
Required: true,
},
"source_cidr": schema.StringAttribute{
Required: true,
},
"description": schema.StringAttribute{
Optional: true,
},
},
},
},
HCL Usage
resource "mycloud_firewall" "example" {
name = "web-firewall"
rule {
port = 80
protocol = "tcp"
source_cidr = "0.0.0.0/0"
description = "HTTP"
}
rule {
port = 443
protocol = "tcp"
source_cidr = "0.0.0.0/0"
description = "HTTPS"
}
}
Model Definition
type FirewallModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Rules types.List `tfsdk:"rule"` // List of RuleModel
}
type RuleModel struct {
Port types.Number `tfsdk:"port"`
Protocol types.String `tfsdk:"protocol"`
SourceCIDR types.String `tfsdk:"source_cidr"`
Description types.String `tfsdk:"description"`
}
Working with ListNestedAttribute
func (r *FirewallResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan FirewallModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
// Extract rules
var rules []RuleModel
if !plan.Rules.IsNull() {
diags := plan.Rules.ElementsAs(ctx, &rules, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Convert to API format
apiRules := make([]APIRule, len(rules))
for i, rule := range rules {
port, _ := rule.Port.ValueBigFloat().Int64()
apiRules[i] = APIRule{
Port: int(port),
Protocol: rule.Protocol.ValueString(),
SourceCIDR: rule.SourceCIDR.ValueString(),
Description: rule.Description.ValueString(),
}
}
// Create firewall with rules
firewall, err := r.client.CreateFirewall(ctx, CreateFirewallRequest{
Name: plan.Name.ValueString(),
Rules: apiRules,
})
// ... handle response ...
}
SetNestedAttribute
Use for one-to-many unordered, unique relationships.
Schema Definition
"tag": schema.SetNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"key": schema.StringAttribute{
Required: true,
},
"value": schema.StringAttribute{
Required: true,
},
},
},
},
HCL Usage
resource "mycloud_resource" "example" {
name = "myresource"
tag {
key = "Environment"
value = "Production"
}
tag {
key = "Team"
value = "Platform"
}
}
Model Definition
type ResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Tags types.Set `tfsdk:"tag"` // Set of TagModel
}
type TagModel struct {
Key types.String `tfsdk:"key"`
Value types.String `tfsdk:"value"`
}
Working with SetNestedAttribute
func (r *MyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan ResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
// Extract tags
var tags []TagModel
if !plan.Tags.IsNull() {
diags := plan.Tags.ElementsAs(ctx, &tags, false)
resp.Diagnostics.Append(diags...)
}
// Convert to map for API
tagMap := make(map[string]string)
for _, tag := range tags {
tagMap[tag.Key.ValueString()] = tag.Value.ValueString()
}
// Create resource with tags
resource, err := r.client.CreateResource(ctx, CreateResourceRequest{
Name: plan.Name.ValueString(),
Tags: tagMap,
})
// ... handle response ...
}
MapNestedAttribute
Use for key-value nested structures.
Schema Definition
"metadata": schema.MapNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Required: true,
},
"value": schema.StringAttribute{
Required: true,
},
},
},
},
HCL Usage
resource "mycloud_app" "example" {
name = "myapp"
metadata = {
env = {
type = "string"
value = "production"
}
version = {
type = "number"
value = "1.0"
}
}
}
Deeply Nested Schemas
Three-Level Nesting
"deployment": schema.SingleNestedAttribute{
Required: true,
Attributes: map[string]schema.Attribute{
"strategy": schema.StringAttribute{
Required: true,
},
"rolling_update": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"max_surge": schema.NumberAttribute{
Optional: true,
},
"max_unavailable": schema.NumberAttribute{
Optional: true,
},
"health_check": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"path": schema.StringAttribute{
Required: true,
},
"interval": schema.NumberAttribute{
Optional: true,
},
},
},
},
},
},
},
resource "mycloud_service" "example" {
name = "myservice"
deployment {
strategy = "rolling"
rolling_update {
max_surge = 1
max_unavailable = 0
health_check {
path = "/health"
interval = 30
}
}
}
}
Model Definitions
type ServiceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Deployment *DeploymentModel `tfsdk:"deployment"`
}
type DeploymentModel struct {
Strategy types.String `tfsdk:"strategy"`
RollingUpdate *RollingUpdateModel `tfsdk:"rolling_update"`
}
type RollingUpdateModel struct {
MaxSurge types.Number `tfsdk:"max_surge"`
MaxUnavailable types.Number `tfsdk:"max_unavailable"`
HealthCheck *HealthCheckModel `tfsdk:"health_check"`
}
type HealthCheckModel struct {
Path types.String `tfsdk:"path"`
Interval types.Number `tfsdk:"interval"`
}
Common Patterns
Pattern 1: Optional Configuration Block
"advanced": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"connection_timeout": schema.NumberAttribute{
Optional: true,
},
"max_retries": schema.NumberAttribute{
Optional: true,
},
},
},
# Most users omit this block
resource "mycloud_app" "simple" {
name = "simple-app"
}
# Advanced users configure it
resource "mycloud_app" "advanced" {
name = "advanced-app"
advanced {
connection_timeout = 60
max_retries = 5
}
}
Pattern 2: Multiple Network Interfaces
"network_interface": schema.ListNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"subnet_id": schema.StringAttribute{
Required: true,
},
"private_ip": schema.StringAttribute{
Optional: true,
Computed: true,
},
"public_ip": schema.StringAttribute{
Computed: true,
},
},
},
},
resource "mycloud_instance" "example" {
name = "multi-nic"
network_interface {
subnet_id = "subnet-123"
private_ip = "10.0.1.10"
}
network_interface {
subnet_id = "subnet-456"
private_ip = "10.0.2.10"
}
}
Pattern 3: Scaling Configuration
"autoscaling": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"min_instances": schema.NumberAttribute{
Required: true,
},
"max_instances": schema.NumberAttribute{
Required: true,
},
"target_cpu": schema.NumberAttribute{
Optional: true,
},
"scale_down_cooldown": schema.NumberAttribute{
Optional: true,
},
},
},
resource "mycloud_app" "example" {
name = "scalable-app"
autoscaling {
min_instances = 2
max_instances = 10
target_cpu = 70
scale_down_cooldown = 300
}
}
Pattern 4: Environment Variables
"environment": schema.ListNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"value": schema.StringAttribute{
Required: true,
Sensitive: true,
},
},
},
},
resource "mycloud_function" "example" {
name = "myfunction"
environment {
name = "DATABASE_URL"
value = var.db_url
}
environment {
name = "API_KEY"
value = var.api_key
}
}
Pattern 5: Volume Mounts
"volume": schema.ListNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"mount_path": schema.StringAttribute{
Required: true,
},
"size_gb": schema.NumberAttribute{
Required: true,
},
"type": schema.StringAttribute{
Optional: true,
},
},
},
},
resource "mycloud_container" "example" {
name = "mycontainer"
volume {
name = "data"
mount_path = "/data"
size_gb = 100
type = "ssd"
}
volume {
name = "logs"
mount_path = "/var/log"
size_gb = 50
}
}
Validation in Nested Schemas
Nested Attribute Validation
"port_range": schema.ListNestedAttribute{
Optional: true,
Validators: []validator.List{
listvalidator.SizeAtMost(10), // Max 10 port ranges
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"from": schema.NumberAttribute{
Required: true,
Validators: []validator.Number{
numbervalidator.Between(1, 65535),
},
},
"to": schema.NumberAttribute{
Required: true,
Validators: []validator.Number{
numbervalidator.Between(1, 65535),
},
},
},
},
},
Cross-Field Validation in Nested Blocks
func (r *MyResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var config MyResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
// Extract port ranges
var ranges []PortRangeModel
if !config.PortRanges.IsNull() {
config.PortRanges.ElementsAs(ctx, &ranges, false)
}
// Validate from <= to for each range
for i, r := range ranges {
if !r.From.IsNull() && !r.To.IsNull() {
from, _ := r.From.ValueBigFloat().Int64()
to, _ := r.To.ValueBigFloat().Int64()
if from > to {
resp.Diagnostics.AddError(
"Invalid Port Range",
fmt.Sprintf("port_range[%d]: 'from' (%d) must be less than or equal to 'to' (%d)", i, from, to),
)
}
}
}
}
Best Practices
1. Limit Nesting Depth
// ✅ Good: 2-3 levels
deployment {
rolling_update {
max_surge = 1
}
}
// ❌ Bad: 5+ levels (confusing)
level1 {
level2 {
level3 {
level4 {
level5 {
value = "too deep"
}
}
}
}
}
2. Use Meaningful Block Names
// ✅ Good: Clear names
"network_interface": schema.ListNestedAttribute{...}
"security_rule": schema.ListNestedAttribute{...}
// ❌ Bad: Vague names
"config": schema.ListNestedAttribute{...}
"item": schema.ListNestedAttribute{...}
3. Choose Appropriate Nested Type
// ✅ Good: Single for one config
"database": schema.SingleNestedAttribute{...}
// ✅ Good: List for multiple ordered
"deployment_stage": schema.ListNestedAttribute{...}
// ✅ Good: Set for multiple unordered unique
"tag": schema.SetNestedAttribute{...}
4. Document Nested Structures
"network": schema.SingleNestedAttribute{
Optional: true,
MarkdownDescription: "Network configuration for the application. If omitted, default network settings will be used.",
Attributes: map[string]schema.Attribute{
"vpc_id": schema.StringAttribute{
Required: true,
MarkdownDescription: "ID of the VPC to deploy into",
},
},
},
5. Provide Examples
Document nested structure usage in provider documentation.
Common Mistakes
❌ Mistake 1: Too Much Nesting
// Bad: 6 levels deep
resource {
level1 {
level2 {
level3 {
level4 {
level5 {
level6 {
value = "help!"
}
}
}
}
}
}
}
❌ Mistake 2: Wrong Nested Type
// Bad: Set for ordered items
"deployment_stage": schema.SetNestedAttribute{ // Should be List!
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"order": schema.NumberAttribute{...},
"name": schema.StringAttribute{...},
},
},
}
❌ Mistake 3: Not Checking for Null
// Bad: Assumes network is always present
vpcID := plan.Network.VpcID.ValueString() // Panics if Network is nil!
// Good: Check for null
if plan.Network != nil {
vpcID := plan.Network.VpcID.ValueString()
}
Key Takeaways
- ✅ Use SingleNestedAttribute for one-to-one relationships
- ✅ Use ListNestedAttribute for ordered collections
- ✅ Use SetNestedAttribute for unordered unique collections
- ✅ Use MapNestedAttribute for key-value structures
- ✅ Limit nesting depth to 2-3 levels
- ✅ Always check for null before accessing nested attributes
- ✅ Validate nested attributes appropriately
- ✅ Document complex nested structures
- ✅ Choose nested types based on use case
What’s Next?
Now let’s review schema design best practices.
Previous: ← Sensitive Attributes
Last updated: June 30, 2026