Error Accessing Set values using the Terraform JSON syntax

:wave: Hi everyone,

I’m creating a library that builds .tf.json files as an alternative to HCL. Everything is going pretty smoothly, but I’m stuck on a problem I cannot find a solution to.

When I try to access a Set value like so:

{ 
  "resource": {
      "subdomains_record_valid_test1": {
          "name": "${resource.aws_acm_certificate.subdomains_cert_test1.domain_validation_options[0].resource_record_name}",
          "type": "${resource.aws_acm_certificate.subdomains_cert_test1.domain_validation_options[0].resource_record_type}",
          "zone_id": "${data.aws_route53_zone.subdomains_zone.zone_id}",
          "ttl": 60,
          "allow_overwrite": true,
          "records":  ["${resource.aws_acm_certificate.subdomains_cert_test1.domain_validation_options[0].resource_record_value}"]
         }
    }
}

I get this error message when I terraform plan or apply:

Elements of a set are identified only by their value and don't have any separate index or key to select with, so it's only possible to perform operations across all elements of the set.

I’ve looked through the docs as well as this from the upgrade guide:

Configuration references to this attribute will likely require updates since sets cannot be
indexed (e.g., domain_validation_options[0] or the older domain_validation_options.0.
syntax will return errors). If the domain_validation_options list previously contained only a
single element like the two examples just shown, it may be possible to wrap these
references using the tolist() function

(e.g., tolist(aws_acm_certificate.example.domain_validation_options)[0])

Is there a way to use functions in JSON? There’s not much to speak of specifically for the JSON syntax, for which I found only this:

The translation of attribute and output values is the same intuitive mapping from HCL types to JSON types
used by Terraform’s jsonencode
function. This mapping does lose some information: lists, sets, and tuples all lower to JSON arrays while
maps and objects both lower to JSON objects. Unknown values and null values are both treated as absent or null.

Any pointers to how I might break into a set via the JSON syntax would be very much appreciated :pray:

Ok, I got it working with this:

{ 
  "resource": {
      "subdomains_record_valid_test1": {
          "name": "${tolist(resource.aws_acm_certificate.subdomains_cert_test1.domain_validation_options)[0].resource_record_name}",
          "type": "${tolist(resource.aws_acm_certificate.subdomains_cert_test1.domain_validation_options)[0].resource_record_type}",
          "zone_id": "${data.aws_route53_zone.subdomains_zone.zone_id}",
          "ttl": 60,
          "allow_overwrite": true,
          "records":  ["${tolist(resource.aws_acm_certificate.subdomains_cert_test1.domain_validation_options)[0].resource_record_value}"]
         }
    }
}

Thanks for sharing your solution, @loganpowell!

If you are certain that there will always be only one element in this set (which the code you shared seems to be assuming but not actually checking) then an alternative to tolist(x)[0] would be one(x), which has three possible outcomes:

  • If x is a single-element collection (sets are a kind of collection) then it returns the value of the single element.
  • If x is a zero-element collection then it returns null.
  • Otherwise, it raises an error.

It should therefore be equivalent to your tolist(x)[0] solution except that it will raise an error if your assumption of only one element turns out not to hold.

1 Like

Fantastic, thank you sir, and thank you for being a great shepherd.

Confirmed that your one(x) approach works :smile: