[SOLVED] New way to set Optional Object Type Attributes causes headache

To be honest, I still don’t understand why so many people didn’t like the experimental defaults() function. Perhaps I just did not delve into it very much or did not want to do it, but this function satisfied all my requests. Now, when this function has been replaced by a new, supposedly simpler way to set default values ​​(it really looks simpler, it just works in a completely different way), on the contrary, I ran into problems.

Perhaps someone can suggest how, when using the new way of setting default values for optional attributes, I can understand “who” set the values of these attributes - the users themselves in their configuration, or were the default values ​​applied?

For example, I have a variable like this:

variable "page_rules" {
  type = list(object({
    page_rule_name = string
    target         = string
    actions = object({
      minify = optional(object({
        html = optional(string, "off")
        css  = optional(string, "off")
        js   = optional(string, "off")
      }))
      mirage = optional(string)
      polish = optional(string)
    })
    priority = optional(number)
    status   = optional(string)
  }))
  default = []

  validation {
    condition = alltrue(flatten([
      for page_rule in var.page_rules : anytrue([
        for action in page_rule.actions : action != null
      ])
    ]))
    error_message = "Error details: The action object of each rule must contain at least one non-null action."
  }
}

Previously, I was able to stop the user from using the following configuration simply because it doesn’t look normal and doesn’t make sense:

page_rules = [
  {
    page_rule_name = "rule_1"
    target         = "acme.com"
    actions = {}
  },
  {
    page_rule_name = "rule_2"
    target         = "acme.com/login"
    actions = {}
  }
]

However, now I can’t do that because the validation rule won’t work due to the way the default values are now applied, each actions object will now have:

minify = {
  html = "off"
  css  = "off"
  js   = "off"
}

How can I find out what exactly the user wrote in the configuration?

Hi @alex-feel,

This mechanism is designed to unify the handling of optional arguments for resources with how they work for object values in input variables.

It’s intentionally impossible for a provider to distinguish between an argument being unset and it being null, because distinguishing those would make it impossible for a caller to conditionally set the argument using a conditional operator where one of the result arms is null.

However, I can see that the absence of a function like defaults makes it harder for a module author to mimick what a provider developer might do to apply default values only after they’ve checked what is and is not null. Provider developers are writing in an imperative language where they can easily write a helper function to plug in all of the default values at once, which I think is roughly what you were doing with defaults before. In current Terraform this must be done only one attribute at a time using the coalesce function.

The confusions around defaults were primarily in how it dealt with null collections vs. non-null collections with nulls inside them. I think it would be worth investigating an alternative version of defaults that doesn’t do the funny stuff with collections and instead assumes that the optional attributes mechanism will deal with that part, so that the defaults function only needs to work with objects and not also with collections. I don’t know what the details of that would be exactly yet, but we can hopefully see some examples in the wild of uses of coalesce to see what the long-winded versions tend to look like and so what a simpler and less confusing design for a function like defaults might look like.

1 Like

Yes, you’re right, so I decided to switch to the coalesce() function. Thank you for all your thoughts :+1: