Description
Module version
v1.2.0
Use-cases
When a framework List/Map/Object/Set value type is present in a data model, e.g. the types.Object
below
type ExampleResourceModel struct {
ID types.String `tfsdk:"id"`
Other types.String `tfsdk:"other"`
SingleNestedAttribute types.Object `tfsdk:"single_nested_attribute"`
}
The zero-value for the value types implicitly cannot contain the correct underlying type implementation for the associated schema, e.g. for this example:
func (r *ExampleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"other": schema.StringAttribute{
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"single_nested_attribute": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"nested_attribute": schema.StringAttribute{
Optional: true,
},
},
Optional: true,
},
},
}
}
The types.Object
zero-value does not contain the underlying nested_attribute
object attribute name or its type.
It is possible for provider developers to run into situations where the zero-value is unknowingly being incorrectly used.
For example with CRUD logic:
func (r ExampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan ExampleResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
// API logic, etc.
// Consider calling plan.XXX = YYY for Computed values instead of
// this approach of instantiating a whole new model.
state := ExampleResourceModel{
ID: plan.ID,
Other: plan.Other,
// Missing SingleNestedAttribute
}
resp.Diagnostics.Append(resp.State.Get(ctx, &state)...)
}
For example with ImportState logic:
func (r ExampleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
var data ExampleResourceModel
data.ID = types.StringValue(req.ID)
data.Other = data.ID
// data.SingleNestedAttribute remains the types.Object zero-value
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
#715 should help somewhat with troubleshooting here, where an error is raised:
Error: Value Conversion Error
An unexpected error was encountered while verifying an attribute value
matched its expected type to prevent unexpected behavior or panics. This is
always an error in the provider. Please report the following to the provider
developer:
Expected type: types.ObjectType["nested_attribute":basetypes.StringType]
Value type: types.ObjectType[]
Path: single_nested_attribute
But this error is generic due to the generic nature of the shared reflection logic and it might not be clear what the next steps for resolving the issue are in this situation.
Attempted Solutions
Ideally, the provider code is fixed to either use the framework-provided request or response data to fetch the information. In CRUD logic, it may look like:
func (r ExampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan ExampleResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
// API logic, etc.
// Consider calling plan.XXX = YYY for Computed values instead of
// this approach of instantiating a whole new model.
state := ExampleResourceModel{
ID: plan.ID,
Other: plan.Other,
SingleNestedAttribute: types.ObjectNull(plan.SingleNestedAttribute.AttributeTypes(ctx)),
}
resp.Diagnostics.Append(resp.State.Get(ctx, &state)...)
}
In the awkward case of ImportState
:
func (r ExampleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
var data ExampleResourceModel
var obj types.Object // can be avoided with awkward resp.State.Get(ctx, &data) instead
data.ID = types.StringValue(req.ID)
data.Other = data.ID
resp.Diagnostics.Append(resp.State.GetAttribute(ctx, path.Root("single_nested_attribute"), &obj)...)
if resp.Diagnostics.HasError() {
return
}
data.SingleNestedAttribute = types.ObjectNull(obj.AttributeTypes(ctx))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
Another valid fix would be using SetAttribute()
over Set()
so the value is not being overwritten with the zero-value.
func (r ExampleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id := types.StringValue(req.ID)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("other"), id)...)
}
Proposal
One option here would be attempting to detect problematic zero-value value types when Set()
is being called. The prior value should contain all the correct type information and a value state, so the framework could use that information to "ignore" the zero-value errantly being set. The underlying implementation to enable this would likely be quite complex.
It would be debatable whether a warning log or diagnostic should be raised if the framework does implement this behavior. The diagnostic is more ideal, however there is a decent chance it would be encountered by practitioners instead of developers. Developers may not notice a log though.