Semantic Equality, SetAttribute, and configuration changes

I’m having some trouble understanding the exact purpose of implementing semantic equality for a custom type.

It seems like the only thing it is used for is comparing the Read() value to the state value for a resource, so that Terraform knows whether to update the state or leave it as-is. For example, if I’m using timetypes.GoDuration as a custom attribute type, configure a value of "1m", and then Read() returns a value of "60s", Terraform will use semantic equality to determine that the state does not need to be updated for that attribute, and its value will remain "1m" in the stored state.

However, it seems like it is not used when comparing the configured value to the state value. For example, configuring a value of "1m", then later changing it to "60s", will still trigger an Update().

It also does not seem to be used when comparing items in a SetAttribute. For example, configuring a SetAttribute having an ElementType of timetypes.GoDuration with a value of ["1m", "60s"] will cause problems, since the underlying API will often silently deduplicate/normalize the value during Create() and Update(), causing Read() to only return ["1m0s"], which Terraform will incorrectly interpret as configuration drift.

But I also can’t create a custom type which behaves the way I would expect it to in these latter scenarios; if I normalize the value in ValueFromString(), then Terraform raises an error that the planned value does not match the configured value; if I implement Equal() the same way as StringSemanticEquals(), then Terraform incorrectly thinks the value has drifted when it hasn’t.

Is this by design, or unintentional? Is it possible to correctly handle all of these cases the way I would expect them to behave?

I’ve been kicking around a SetWithSemanticEquals attribute because the regular set attribute only calls Equal() when comparing elements of the set.

I don’t know if it’s going to work out the way either of us hope, but maybe take a look?

Hey there @zanecodes, I think for your use-case, what you’d really want to implement is a custom type for a types.Set, rather than a custom type for the element itself (the duration strings).

In your use-case, sounds like the API is normalizing the set (deduplicating the elements) as well as normalizing the elements, so the easiest way to evaluate that would be at the types.Set itself.

To answer your question about what exactly semantic equality will entail, I think you’re understanding what is implemented correctly. Semantic equality is purely a provider/SDK feature that prevents sending values back to Terraform that would cause “drift” when the drift is not actionable (like additional whitespace in strings). This happens after Create, Update, and Read, so if your API deduplicates your types.Set of durations, semantic equality would allow you to not send the deduplicated set back to Terraform, therefore, not stored to state.

What semantic equality doesn’t do, is attempt to ignore configuration changes that may be semantically equal. That’s not technically drift (since the practitioner is actually changing their configuration, signaling intent to potentially make a change), so semantic equality doesn’t do that by default. However, it would be valid for you, in addition to implementing semantic equality, write a plan modifier that:

  • Compares the new configuration value with the prior state
  • If the change requested by the user does not require an update (i.e. the values are semantically equivalent to the API), plan the prior state value. Thus resulting in no proposed diff

You could re-use your SetSemanticEquals function on your custom type to do this.


That isn’t done by default because semantic equality is only attempting to protect Terraform from drift, not from user intent/configuration changes (which typically is king in Terraform). But as mentioned, the provider can always modify the plan and keep prior state for attributes when the provider knows the Update will not change the underlying API data.