My custom type needs more context than is available from the embedded type

I’m working with an API which accepts aliases for some string values.

For example, if the API attribute were a color, it might accept crimson or raspberry, but when the value is retrieved, it will return red.

I can retrieve the full list of possible aliases in Create(), Read(), and Update(), but I’m not able to predict them.

I thought I’d be able to solve this problem with a custom string type which knows about the aliases and implements semantic equality:

type StringWithAlts struct {
	basetypes.StringValue
	altValues []basetypes.StringValue // alt values are aliases for the string value
}

The tests for this custom type work fine. But the altValues element doesn’t survive when used in a provider.

My intent was to populate the altValues struct element in Read(), so that no changes would be detected when the user’s config says crimson and the API responds with red (alts: crimson, raspberry, scarlet)

I’m setting the altValues element in Read(), but find them missing shortly afterward when the StringSemanticEquals() function is invoked.

Maybe I should be encoding the values in such a way that they survive the trip across the RPC API?

I’m a little fuzzy which of these functions should be doing the encoding and decoding if I was to adopt such a strategy:

  • StringWithAltsType.ValueFromTerraform()
  • StringWithAltsType.ValueFromString()
  • StringWithAlts.ToStringValue()
  • StringWithAlts.ToTerraformValue()
  • StringWithAlts.String()

I don’t want to impose strange encoding requirements (JSON, etc…) on the practitioner, and I don’t care if the values in the state file are encoded with alts. I just want to collect a value with alts during Read() and make use of its alts in StringSemanticEquals().

There’s a skeleton provider here. The following configuration applies okay…

terraform {
  required_providers {
    altstrings = {
      source  = "chrismarget/altstrings"
    }
  }
}

resource "altstrings_thing" "test" {
  color = "salmon"
}

…but without modifying the configuration, it generates a plan because of the alias thing:

  # altstrings_thing.test will be updated in-place
  ~ resource "altstrings_thing" "test" {
      ~ color = "red" -> "salmon"
        id    = "50bbfbfb-aa39-4f45-886e-dab9950fe2e0"
    }

So, does encoding the alt values into the string payload make sense? I’m imagining the string value could be something like ["red", "crimson", "raspberry", "scarlet"]

Yeah you won’t be able to encode any data at runtime into a custom type, it’s actually going to be lost the next line when resp.State.Set is called:

	// pretend to have read from the API a hue, but use the custom type with alternate values.
	// e.g. the API returned "red", but we know that's semantically equal to "crimson", "scarlet", etc...
	m.Color = customtype.NewStringWithAlts(hue, colors...)

	resp.Diagnostics.Append(resp.State.Set(ctx, &m)...) // <------- *poof*

If you put a breakpoint in your (StringWithAltsType).ValueFromTerraform function, you’ll see the framework call it to reconstruct the custom value. (which it gets the type from the schema)

One of the reasons for that is because we don’t even use the framework type system to store state/config/plan data: Consider Surfacing Framework Type System Data for Config/Plan/State · Issue #590 · hashicorp/terraform-plugin-framework · GitHub, which is unfortunate. That being said, custom types in general are designed to have all the information they need at compile time. (internally there is a lot of conversion from type to value, from the protocol specifically)


I’m a little fuzzy which of these functions should be doing the encoding and decoding

Decoding → StringWithAltsType.ValueFromTerraform()
Encoding → StringWithAlts.ToTerraformValue()


I’m not sure if you’ll be able to solve this problem with the custom type system without encoding/decoding this data to/from Terraform because of #590. I think the only solution you’d be able to find that doesn’t encode/decode is avoiding custom types and just implementing the semantic logic manually in your CRUD methods :pensive_face:

Thank you, @austin.valle

I suspected something like this was going on, and it explains why ValueFromTerraform() is getting called so often!

I’m going to tinker with serializing the whole struct (with alt values) in the To/From Terraform functions.

I think the only solution you’d be able to find that doesn’t encode/decode is avoiding custom types and just implementing the semantic logic manually in your CRUD methods

Well, it doesn’t sound like you’re cautioning me away from the encoding strategy…

I’m back on this project after 6+ months of distractions. I don’t remember the details, but I decided back then it was useful to solve this using a custom type rather than just lying about what the API response in Read().