Nested Schemas

8 minute read

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.

Next: Schema Best Practices →


Previous: ← Sensitive Attributes

Last updated: June 30, 2026

Updated: