Why is req.Plan.Get() in my Create() method throwing an error?

I’m working with framework 0.11.1, and am stuck looking at a problem where my resource Create() method encounters an error almost immediately.

The error summary is:

Value Conversion Error

The error detail says:

An unexpected error was encountered trying to build a value.
This is always an error in the provider.
Please report the following to the provider developer:

unhandled unknown value

The error occurs right at the beginning of the Create() function, when it fetches the plan:

plan := &ResourceMyThing{}
diags := req.Plan.Get(ctx, plan) // <--- the error is produced right here
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
        return  
}       

Given that there’s no state, and almost none of my code has run yet, I’m guessing it’s a problem with how I’ve set up the schema.

GetSchema () returns the following:

return tfsdk.Schema{
  Attributes: map[string]tfsdk.Attribute{
    "name": {   
      Type:       types.StringType,
      Required:   true,    
      Validators: []tfsdk.AttributeValidator{stringvalidator.LengthAtLeast(1)},
    },          
    "nested_set": {
      Required:   true,    
      Attributes: tfsdk.SetNestedAttributes(map[string]tfsdk.Attribute{
        "name": {               
          Type:       types.StringType,
          Required:   true,    
          Validators: []tfsdk.AttributeValidator{stringvalidator.LengthAtLeast(1)},
        },                      
        "problem_netsted_list": {
          Computed:   true,    
          Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
            "attr_1": {                         
              Computed: true,      
              Type:     types.Int64Type,
            },                                  
            "attr_2": {                         
              Computed: true,      
              Type:     types.Int64Type,
            },                                  
          }                             
        }                       
      }                 
    }           
  }      
}

problem_nested_list does not appear in the plan summary printed by terraform plan (this feels like a big clue), and if I drop it from the schema, everything works the way I expect.

What should I be doing differently?

Thanks!

I realized I should have included the data structure I was trying to use as the plan:

type ResourceMyThing struct {    
        Name      types.String `tfsdk:"name"`
        NestedSet []NS         `tfsdk:"nested_set"`
}

type NS struct {
        Name              types.String `tfsdk:"name"`
        ProblemNestedList []PNL        `tfsdk:"problem_nested_list"`
}

type PNL struct {
        Attr1 types.Int64 `tfsdk:"attr_1"`
        Attr2 types.Int64 `tfsdk:"attr_2"`
}

Also, I suspect the fix I’m looking for is lurking in this issue, but I haven’t worked it out yet.

Hi @bumble :wave:

The unhandled unknown value error is an unfortunately terse error message about the provider-defined structs to read plan data not using types types, which are compatible with Terraform’s null, unknown, or known values. Additional information about this situation can be found in the current documentation here: Plugin Development - Framework: Access State, Config, and Plan | Terraform by HashiCorp

So in this case, adjusting those types to something like:

type ResourceMyThing struct {    
        Name      types.String `tfsdk:"name"`
        NestedSet types.Set   `tfsdk:"nested_set"`
}

type NS struct {
        Name              types.String `tfsdk:"name"`
        ProblemNestedList types.List   `tfsdk:"problem_nested_list"`
}

Should get past that particular error. It’s unfortunately a little more work on the provider side to then extract the “next level” of data using methods like (types.List).ElementsAs() or (types.Object).As(), but hopefully that unblocks you from your current situation.

I’d previously gotten the impression I should explore types.Object, types.Set, etc…, but wasn’t sure how to handle the “next level” problem you mentioned.

This looks like the hint I needed.

Thank you!

Making progress, thank you @bflad.

I noticed in HashiTalks: A Modern Terraform Plugin Framework, that you’re using various native Go types in the structures packed and unpacked by Terraform (16:37 timestamp, for example).

  1. Is the recommended approach to use native Go types/structs where possible and reserve the types types for cases where they’re needed for compatibility with unknown/null values?

  2. I’ve implemented the same feature (ratings) three times in my practice provider’s code below. Is one style preferred over the other? Any obvious problems (other than the case which doesn’t handle unknown/null) here? I’m leaning toward Ratings1 style for nested attributes which must handle unknown.

  3. My least-preferred option (Ratings2) doesn’t have MarkdownDescription for the source and value schema elements because I couldn’t figure out where to put them. Do they have a home somewhere when using this structure?

The server’s data:

{
  "imdbID": "tt0103064",
  "Title": "Terminator 2: Judgment Day",
  "Ratings": [
    {
      "Source": "Internet Movie Database",
      "Value": "8.6/10"
    },
    {
      "Source": "Rotten Tomatoes",
      "Value": "93%"
    }
  ],
}

The Go data structures:

type filmByIdData struct {
	ImdbId   types.String     `tfsdk:"imdb_id"`
	Title    types.String     `tfsdk:"title"`
	Ratings0 []filmRatingData `tfsdk:"ratings0"`
	Ratings1 []types.Object   `tfsdk:"ratings1"`
	Ratings2 types.List       `tfsdk:"ratings2"`
}

type filmRatingData struct {
	Source types.String `tfsdk:"source"`
	Value  types.String `tfsdk:"value"`
}

Schema:

tfsdk.Schema{
		MarkdownDescription: "This Data Source returns details about a film by its IMDb ID.",
		Attributes: map[string]tfsdk.Attribute{
			"imdb_id": {
				MarkdownDescription: "Unique ID used by both OMDb and IMDb",
				Required:            true,
				Type:                types.StringType,
			},
			"title": {
				MarkdownDescription: "Film title",
				Computed:            true,
				Type:                types.StringType,
			},
			"ratings0": {
				MarkdownDescription: "Ratings0",
				Computed:            true,
				Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
					"source": {
						MarkdownDescription: "Review source",
						Computed:            true,
						Type:                types.StringType,
					},
					"value": {
						MarkdownDescription: "Review value",
						Computed:            true,
						Type:                types.StringType,
					},
				}),
			},
			"ratings1": {
				MarkdownDescription: "Ratings1",
				Computed:            true,
				Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
					"source": {
						MarkdownDescription: "Review source",
						Computed:            true,
						Type:                types.StringType,
					},
					"value": {
						MarkdownDescription: "Review value",
						Computed:            true,
						Type:                types.StringType,
					},
				}),
			},
			"ratings2": {
				MarkdownDescription: "Ratings2 -- put full description of the included object (source and value) here, I guess?",
				Computed:            true,
				Type: types.ListType{
					ElemType: types.ObjectType{
						AttrTypes: map[string]attr.Type{
							"source": types.StringType,
							"value":  types.StringType,
						},
					},
				},
			},
		},
	}

When the datasource.DataSource creates state in Read(), it does this:

	state := filmByIdData{
		ImdbId: types.String{Value: config.ImdbId.Value},
		Title:  types.String{Value: apiResponse.Title},
		//Ratings0: []filmRatingData{},
		//Ratings1: []types.Object{},
		//Ratings2: types.List{},
	}

	state.Ratings0 = make([]filmRatingData, len(apiResponse.Ratings))
	for i, rating := range apiResponse.Ratings {
		state.Ratings0[i] = filmRatingData{
			Source: types.String{Value: rating.Source},
			Value:  types.String{Value: rating.Value},
		}
	}

	state.Ratings1 = make([]types.Object, len(apiResponse.Ratings))
	for i, rating := range apiResponse.Ratings {
		state.Ratings1[i] = types.Object{
			AttrTypes: map[string]attr.Type{
				"source": types.StringType,
				"value":  types.StringType,
			},
			Attrs: map[string]attr.Value{
				"source": types.String{Value: rating.Source},
				"value":  types.String{Value: rating.Value},
			},
		}
	}

	state.Ratings2 = types.List{
		Elems: make([]attr.Value, len(apiResponse.Ratings)),
		ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{
			"source": types.StringType,
			"value":  types.StringType,
		}},
	}
	for i, rating := range apiResponse.Ratings {
		state.Ratings2.Elems[i] = types.Object{
			AttrTypes: map[string]attr.Type{
				"source": types.StringType,
				"value":  types.StringType,
			},
			Attrs: map[string]attr.Value{
				"source": types.String{Value: rating.Source},
				"value":  types.String{Value: rating.Value},
			},
		}
	}