Properly validating default values for optional fields in an object

Hello o/

I am trying to validate an object type variable that contains a number of optional fields with defaults. I am having issues validating the fields if only a subset is defined.

Minimal example:

variable "test" {
  type = object({
    foo = optional(string)
    bar = optional(string)
  })
  default = {
    foo = "foo"
    bar = "bar"
  }
  validation {
    condition     = contains(["foo"], var.test.foo)
    error_message = "Invalid foo"
  }
  validation {
    condition     = contains(["bar"], var.test.bar)
    error_message = "Invalid bar"
  }
}

And defining it as so:

  test = {
    foo = "foo"
  }

yields:

│   15:     condition     = contains(["bar"], var.test.bar)
│     ├────────────────
│     │ var.test.bar is null
│ 
│ Invalid value for "value" parameter: argument must not be null.

Validation does not find that var.test.bar is the default value, bar, but rather is null.

What is the proper/recommended way to validate these default values?

fwiw
Solutions like coalesce(var.test.bar, "bar") do not work if the defaults are specified in a local value separately, as we cannot use locals in validation:

Error: Invalid reference in variable validation

  on ..., in variable "test":
  15:     condition     = contains(["bar"], coalesce(var.test.bar, local.default_test.bar))

The condition for variable "test" can only refer to the variable itself,
using var.test.

(+ Ideally I’d like to avoid the extra coupling in validation as it’s already defined in the defaults)

For future readers, I’ve decided to validate directly with null and move defaults to locals.

Something like:

variable "test" {
  type = object({
    foo = optional(string)
    bar = optional(string)
  })
  default = {}
  validation {
    condition     = var.test.foo == null ? true : contains(["foo"], var.test.foo)
    error_message = "Invalid foo"
  }
  validation {
    condition     = var.test.bar == null ? true : contains(["bar"], var.test.bar)
    error_message = "Invalid bar"
  }
}

locals {
  test = defaults(var.test, {
    foo = "foo"
    bar = "bar"
  })
}

and using local.test instead.
I am doing this because I am stuck on tf v1.2.9 for now.

Perhaps with tf version 1.3.0+ the optional(type, default_value) type declaration does not have this issue?

Hi @Sota,

The configuration you’ve shared here tells Terraform that the default value applies only if the entire test input variable isn’t set.

From what you’ve shared it seems like you actually intended for each of those attributes to have their own separate default value, independent of the other. This is how to describe that situation to Terraform:

variable "test" {
  type = object({
    foo = optional(string, "foo")
    bar = optional(string, "bar")
  })
  default  = {}
  nullable = false
}

Some details about the above:

  • Each attribute has its own default value, specified as the optional second argument to the optional modifier.
  • The default value for the entire variable is set to {}, which doesn’t include values for either foo or bar. That is valid because both of those attributes are optional; Terraform will automatically interpret this default value as { foo = "foo", bar = "bar" } by inserting the default values in just the same way as it would if the caller of your module set test = {}.
  • I also set the variable as nullable = false, which is not crucial but it means that if the caller of the module writes test = null then Terraform will treat it the same as setting the variable to its default value.

With the above declaration you can assume that:

  • var.test will never be null. (Terraform will force it to be {foo = "foo", bar = "bar"} if the caller tries to set it to null.)
  • var.test.foo will never be null. (Terraform will force it to be "foo" if not specified)
  • var.test.bar will never be null. (Terraform will force it to be "bar" if not specified)

You can rely on those assumptions in the conditions in your validation blocks, and elsewhere in your module.