How do I use nested structs for resources

This is my schema for the resource:

func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"cluster": schema.SingleNestedAttribute{
				Required: true,
					Attributes: map[string]schema.Attribute{
						"id": schema.StringAttribute{
							Computed:    true,
							Description: "ID of the cluster",
						},
						"name": schema.StringAttribute{
							Required:    true,
							Description: "Name of the cluster",
						},
						"aws": schema.SingleNestedAttribute{
							Optional: true,
							Attributes: map[string]schema.Attribute{
								"role_arn": schema.StringAttribute{
									Optional:    true,
									Description: "Role ARN of the AWS cluster",
								},
								"key_pair": schema.StringAttribute{
									Optional:    true,
									Description: "Key pair of the AWS cluster",
								},
								"tags": schema.MapAttribute{
									ElementType: types.StringType,
									Optional:    true,
									Description: "Tags of the AWS cluster",
								},
							},
						},
					},
			},
		},
	}
}

schema for data source:

func (c *clustersDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"clusters": schema.ListNestedAttribute{
				Computed: true,
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"id": schema.StringAttribute{
							Computed:    true,
							Description: "ID of the cluster",
						},
						"name": schema.StringAttribute{
							Required:    true,
							Description: "Name of the cluster",
						},
						
						"aws": schema.SingleNestedAttribute{
							Computed: true,
							Attributes: map[string]schema.Attribute{
								"role_arn": schema.StringAttribute{
									Computed:    true,
									Description: "Role ARN of the AWS cluster",
								},
								"key_pair": schema.StringAttribute{
									Computed:    true,
									Description: "Key pair of the AWS cluster",
								},
								"tags": schema.MapAttribute{
									ElementType: types.StringType,
									Computed:    true,
									Description: "Tags of the AWS cluster",
								},
							},
						},
					},
				},
			},
		},
	}
}

This is the struct for both data source and resourc:

type ClusterDetails struct {
	ID             types.String `tfsdk:"id"`
	Name           types.String `tfsdk:"name"`

	Aws AWS `tfsdk:"aws"`
}

type AWS struct {
	RoleARN types.String `tfsdk:"role_arn"`
	KeyPair types.String `tfsdk:"key_pair"`
	Tags    types.Map    `tfsdk:"tags"`
}

When I try terraform plan it works just as expected without any error but for TF_LOG=INFO terraform apply im getting this error:

│ Received null value, however the target type cannot handle null values. Use
│ the corresponding `types` package type, a pointer type or a custom type that
│ handles null values.
│ 
│ Path: cluster.aws
│ Target Type: provider.AWS
│ Suggested `types` Type: basetypes.ObjectValue
│ Suggested Pointer Type: *provider.AWS

Hi @KnockOutEZ :wave:

Sorry you ran into trouble here.

The issue is that the type you have specified for the Aws field in the ClusterDetails struct cannot handle null values.

The most straightforward fix is to alter the ClusterDetails struct as follows:

type ClusterDetails struct {
	ID             types.String `tfsdk:"id"`
	Name           types.String `tfsdk:"name"`
	Aws            types.Object `tfsdk:"aws"`
}

Further discussion of an analogous issue can be found in the following post - How count.index works - #2 by austin.valle

Assuming you are using something along the following lines within, for example, a Create method:

	var data ClusterDetails

	diags := req.Plan.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	/* ... */

You should then be able to convert the value contained within data.Aws into an AWS struct by using data.Aws.ObjectAs(). Refer to Plugin Development - Framework: Object Type | Terraform | HashiCorp Developer for further information on data handling for objects.

1 Like

Hey @bendbennett , Thanks for the quick reply!

This is the parent struct:

type clusterResourceModel struct {
	Cluster ClusterDetails `tfsdk:"cluster"`
}

This is my Create method currently:

func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var plan clusterResourceModel
	diags := req.Plan.Get(ctx, &plan)
	resp.Diagnostics.Append(diags...)
	if resp.Diagnostics.HasError() {
		return
	}

    createdCluster, _ := r.client.CreateCluster(ctx, clusterCreationRequest)

	tagElements := make(map[string]attr.Value)
	for k, v := range createdCluster.Aws.Tags {
		tagElements[k] = types.StringValue(v)
	}
	awsTags, _:= types.MapValue(types.StringType, tagElements)
	plan.Cluster.Aws = AWS{
		RoleARN: types.StringValue(createdCluster.Aws.RoleArn),
		KeyPair: types.StringValue(createdCluster.Aws.KeyPair),
		Tags: awsTags,
	}

	plan.Cluster.ID = types.StringValue(createdCluster.ID)
	plan.Cluster.Name = types.StringValue(createdCluster.Name)
		
	diags = resp.State.Set(ctx, plan)
	resp.Diagnostics.Append(diags...)
	if resp.Diagnostics.HasError() {
		return
	}
}

It says there is no method available as .ObjectAs() in plan.Cluster.Aws when I try plan.Cluster.Aws.ObjectAs() after I changed Aws fields type to types.Object.

Note: the clusterCreationRequest is a go-swagger generated struct so it doesnt contain types from terraform-plugin-framework/types. Im using it just to interact with the api client.

Hi @KnockOutEZ,

Apologies, I neglected to notice the Computed: true fields on the schema.

Something like the following should work:

func (e *exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var data clustersData

	diags := req.Plan.Get(ctx, &data)

	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	awsElementTypes := map[string]attr.Type{
		"role_arn": types.StringType,
		"key_pair": types.StringType,
		"tags": types.MapType{
			ElemType: types.StringType,
		},
	}

	tags, diags := types.MapValue(
		types.StringType,
		map[string]attr.Value{
			"key1": types.StringValue("value1"),
		},
	)

	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	awsElements := map[string]attr.Value{
		"role_arn": types.StringValue("arn:aws:iam::123456789012:role/eks-service-role-AWSServiceRoleForAmazonEKS-123456789012"),
		"key_pair": types.StringValue("eks-key-pair"),
		"tags":     tags,
	}

	awsObjectValue, diags := types.ObjectValue(awsElementTypes, awsElements)

	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	clusterDetailsElementTypes := map[string]attr.Type{
		"id":   types.StringType,
		"name": types.StringType,
		"aws": types.ObjectType{
			AttrTypes: awsElementTypes,
		},
	}

	clusterDetailsElements := map[string]attr.Value{
		"id":   types.StringValue("id"),
		"name": types.StringValue("name"),
		"aws":  awsObjectValue,
	}

	clusterDetailsObjectValue, diags := types.ObjectValue(clusterDetailsElementTypes, clusterDetailsElements)

	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	listValue, diags := types.ListValue(types.ObjectType{AttrTypes: clusterDetailsElementTypes}, []attr.Value{clusterDetailsObjectValue})

	if resp.Diagnostics.HasError() {
		return
	}

	data.Clusters = listValue

	tflog.Trace(ctx, "created a resource")

	diags = resp.State.Set(ctx, &data)
	resp.Diagnostics.Append(diags...)
}

There’s further discussion around data handling in the docs:

I believe that the name attribute should also be Computed: true as you’re setting this with the data that you’re obtaining by calling r.client.CreateCluster():

	"name": schema.StringAttribute{
		Computed:    true,
		Description: "Name of the cluster",
	},
1 Like

Thanks @bendbennett , it solved my issue. Appreciate the help mate

1 Like

Hey @bendbennett Im having a related issue with type.Object. Described in here: How to transform types.Object and []types.Object to golang struct type in resource