JSON in Terraform: HCL vs JSON Syntax, Policies & jsonencode()
Terraform supports JSON as a first-class alternative to HCL (HashiCorp Configuration Language) — every .tf file can be written as a .tf.json file with identical semantics. More commonly, Terraform uses JSON within HCL via the built-in jsonencode() function to embed AWS IAM policy JSON, assume-role documents, and other JSON strings directly in resource arguments. jsondecode() converts a JSON string into a Terraform value for use in expressions. The terraform.tfvars.json file passes variable values as JSON instead of HCL syntax — useful for CI/CD pipelines that generate variables programmatically. Terraform 1.x (the current stable version as of 2025) processes .tf.json files with the same parser as .tf files; the JSON schema mirrors the HCL block structure with an extra nesting level. jsonencode() is the most-used JSON function: it appears in nearly every AWS, GCP, and Azure Terraform module for resource policies. This guide covers the HCL/JSON equivalence, jsonencode()/jsondecode(), variable files, data source outputs, and common patterns.
Need to validate or pretty-print a JSON policy from your Terraform module? Jsonic's formatter handles it instantly.
Open JSON Formatterjsonencode() for Inline Policies
jsonencode() converts a Terraform value (object, map, list) to a compact JSON string. It is used in aws_iam_role_policy.policy, aws_s3_bucket_policy.policy, and aws_sns_topic_policy.policy — essentially anywhere a Terraform resource argument expects a JSON string. The function accepts any Terraform value and produces valid, compact JSON with all special characters properly escaped, so you never need to manually quote or escape values inside the object.
The most common use case is an IAM assume-role trust policy. The following example creates an IAM role with a trust policy that allows EC2 instances to assume it, using a Statement array with 1 entry:
resource "aws_iam_role" "ec2_role" {
name = "ec2-instance-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}
]
})
}
# S3 bucket policy — reference a resource ARN directly
resource "aws_s3_bucket_policy" "example" {
bucket = aws_s3_bucket.example.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { AWS = aws_iam_role.ec2_role.arn }
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "${aws_s3_bucket.example.arn}/*"
}
]
})
}Terraform validates the HCL syntax of the object passed to jsonencode()but does not validate the JSON policy semantics — use the AWS IAM Policy Simulator separately to test policy logic. Multiline JSON with jsonencode() is more readable than combining file() with template strings because variables, locals, and resource attributes are interpolated directly without a separate template syntax. The function is available in all Terraform versions 0.12 and above; Terraform 1.x processes it identically. According to the Terraform registry, over 90% of AWS provider modules use jsonencode() for at least 1 policy argument.
jsondecode() for Data Sources
jsondecode() converts a JSON string into a Terraform value — object, map, list, string, number, or boolean depending on the JSON content. Its most common use is parsing JSON secrets from AWS Secrets Manager, where a single secret stores multiple fields (username, password, host) as a JSON object.
data "aws_secretsmanager_secret_version" "db" {
secret_id = "prod/db/credentials"
}
locals {
db_creds = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}
resource "aws_db_instance" "main" {
identifier = "prod-db"
username = local.db_creds.username
password = local.db_creds.password
# ...
}Access fields with dot notation (local.db_creds.username) or bracket notation (local.db_creds["username"]) — both are valid in Terraform expressions. Use lookup(local.db_creds, "port", 5432) for optional keys with a default value; accessing a missing key directly throws an error at plan time. Note: sensitive values passed through jsondecode() retain the sensitive mark — Terraform redacts them in plan output and state, so credentials are not exposed in logs. This is 1 of the 3 ways Terraform handles secrets: Secrets Manager via jsondecode(), SSM Parameter Store via data.aws_ssm_parameter, and environment variables via TF_VAR_ prefix.
# Safe optional key access with lookup()
locals {
db_port = lookup(local.db_creds, "port", 5432)
db_host = lookup(local.db_creds, "host", "localhost")
}
# jsondecode() also works on inline JSON strings for testing
locals {
example = jsondecode("{"region":"us-east-1","count":3}")
# example.region == "us-east-1"
# example.count == 3
}HCL vs JSON Syntax (.tf.json)
Every HCL construct has a JSON equivalent. Terraform processes .tf.json files with the same parser as .tf files — they can coexist in the same directory and Terraform merges them into a single configuration. The JSON schema adds 1 extra nesting level compared to HCL: block type → labels → attributes.
| Construct | HCL (.tf) | JSON (.tf.json) |
|---|---|---|
| Resource block | resource "aws_s3_bucket" "b" { } | {"resource":{"aws_s3_bucket":{"b":{}}}} |
| Variable | variable "region" { default = "us-east-1" } | {"variable":{"region":{"default":"us-east-1"}}} |
| Output | output "id" { value = aws_s3_bucket.b.id } | {"output":{"id":{"value":"${aws_s3_bucket.b.id}"}}} |
| Provider | provider "aws" { region = "us-east-1" } | {"provider":{"aws":{"region":"us-east-1"}}} |
Labels (the quoted strings after the block type in HCL) become JSON object keys. Attributes are JSON values. Functions and expressions like var.name work the same in both formats — Terraform evaluates them identically regardless of file format. The "${expression}" template syntax works inside JSON string values. Use .tf.json when generating Terraform config from scripts (Python, Node.js, CI pipelines) because all major languages have native JSON serializers. The json.dumps() in Python or JSON.stringify() in Node.js produces valid .tf.json input without requiring an HCL library. Terraform requires at least 1 .tf or .tf.json file to exist before running terraform init.
# main.tf.json — complete provider + resource example
{
"terraform": {
"required_providers": {
"aws": {
"source": "hashicorp/aws",
"version": "~> 5.0"
}
}
},
"provider": {
"aws": {
"region": "${var.region}"
}
},
"variable": {
"region": {
"type": "string",
"default": "us-east-1"
}
},
"resource": {
"aws_s3_bucket": {
"example": {
"bucket": "my-terraform-json-bucket"
}
}
}
}terraform.tfvars.json
Variable values can be supplied as JSON instead of HCL syntax by creating a terraform.tfvars.json file. Terraform auto-loads this file alongside terraform.tfvars — no extra -var-file flag is needed. The file contains a flat JSON object where each key is a declared variable name and the value is the variable value.
# terraform.tfvars.json
{
"region": "us-east-1",
"instance_count": 3,
"tags": {
"Environment": "production",
"Team": "platform"
},
"allowed_cidrs": ["10.0.0.0/8", "172.16.0.0/12"]
}This is more useful than .tfvars in CI/CD because generating JSON is straightforward in any language. The following jq command produces a terraform.tfvars.json file from environment variables in a pipeline:
# Generate terraform.tfvars.json in CI/CD
jq -n --arg region "$AWS_REGION" --argjson count "$INSTANCE_COUNT" --arg env "$ENVIRONMENT" '{
"region": $region,
"instance_count": $count,
"tags": { "Environment": $env }
}' > terraform.tfvars.jsonTerraform also auto-loads any file matching *.auto.tfvars.json — useful for splitting variables across multiple files (e.g. network.auto.tfvars.json, compute.auto.tfvars.json). Secret variables — passwords, tokens, API keys — should come from TF_VAR_name environment variables, not committed JSON files. For example, TF_VAR_db_password=secret sets the db_password variable without touching the file system. Terraform reads TF_VAR_ variables with higher precedence than .tfvars.json, so they safely override default values in checked-in files.
JSON in Terraform Outputs and Data Sources
Terraform outputs can export JSON strings using jsonencode(), which is useful when downstream systems (other Terraform modules, CI pipelines, Lambda functions) need to consume configuration as a single JSON blob. Data sources can produce JSON that jsondecode() then makes available as structured values.
# Export policy as a JSON string output
locals {
policy = {
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:GetObject"]
Resource = "${aws_s3_bucket.example.arn}/*"
}
]
}
}
output "policy_json" {
value = jsonencode(local.policy)
}
# data.aws_iam_policy_document — HCL-native IAM JSON alternative
data "aws_iam_policy_document" "s3_read" {
statement {
effect = "Allow"
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.example.arn}/*"]
}
}
resource "aws_iam_role_policy" "s3_read" {
name = "s3-read"
role = aws_iam_role.example.id
policy = data.aws_iam_policy_document.s3_read.json
}data.aws_iam_policy_document generates validated IAM JSON using HCL blocks with typed fields (effect, actions, resources, principals) and exposes it via the .json attribute — no jsonencode() needed. This is the recommended pattern for IAM policies in production modules. For CloudFormation stack outputs, Terraform reads them via data.aws_cloudformation_stack and the .outputs map — use jsondecode() if any output value is itself a JSON string. Combine jsondecode() and lookup() for safe optional key access with defaults: 3 lines replace what would otherwise be a try() + can() expression chain. Learn more about JSON Schema validation for the JSON structures you embed in Terraform resources.
Key terms
- jsonencode()
- A built-in Terraform function that converts a Terraform value (object, map, list, string, number, or boolean) into a compact JSON string with all special characters properly escaped.
- jsondecode()
- A built-in Terraform function that parses a JSON string and returns the equivalent Terraform value — object, map, list, string, number, or boolean — for use in Terraform expressions and resource arguments.
- HCL (HashiCorp Configuration Language)
- The native configuration language used in
.tffiles; a structured language with block syntax, expressions, and functions designed for human-readable infrastructure declarations. - .tf.json
- A Terraform configuration file written in JSON format instead of HCL; fully equivalent to
.tffiles and processed by the same parser, with an extra JSON nesting level for block labels. - terraform.tfvars.json
- A JSON file that Terraform auto-loads to supply variable values; the JSON keys must match declared variable names, and the values must be compatible with the declared variable types.
- aws_iam_policy_document
- An AWS provider data source that generates IAM-compatible JSON policy documents from typed HCL blocks, validates the IAM structure at plan time, and exposes the result via a
.jsonattribute. - TF_VAR_ prefix
- An environment variable convention that Terraform uses to supply variable values at runtime; a variable named
regioncan be set via the environment variableTF_VAR_regionwithout creating any file.
Frequently asked questions
What is the difference between jsonencode() and file() for Terraform policies?
file("policy.json") reads a JSON file as a raw string — no validation, no variable interpolation. jsonencode({...}) builds the JSON from a Terraform object — supports variables, locals, and expressions. For policies that reference Terraform values (like a bucket ARN), use jsonencode(). For static policies with no dynamic values, file() is simpler. When the policy needs to reference a resource ARN like aws_s3_bucket.example.arn, jsonencode() is required because file() cannot evaluate Terraform expressions embedded in the file content. templatefile() is a middle ground — it reads a file and performs string substitution — but jsonencode() produces guaranteed-valid JSON syntax and is the recommended pattern in the official AWS Terraform modules. Always run terraform validate after writing a new policy to catch syntax errors before plan.
Can I write Terraform configuration in JSON instead of HCL?
Yes. Terraform accepts .tf.json files with the same semantics as .tf HCL files. The JSON schema mirrors HCL blocks with an extra nesting level: block type → labels → attributes. For example, the HCL block resource "aws_s3_bucket" "example" {} becomes {"resource":{"aws_s3_bucket":{"example":{}}}} in JSON. This is useful for programmatically generating Terraform config from Python or Node.js scripts, from CI/CD pipelines, or from tools that output JSON but not HCL. Functions and expressions (like var.name) work the same in both formats — Terraform evaluates them identically. The .tf.json format is officially supported and documented by HashiCorp; it is a first-class alternative, not a workaround. For human-authored config, HCL is more readable. For machine-generated config, .tf.json is better because most languages have native JSON serializers.
How do I use a JSON file for Terraform variable values?
Create terraform.tfvars.json with {"var_name":"value"} keys. Terraform auto-loads it alongside .tfvars files without any extra flags. For CI/CD pipelines, generate this file with jq or Python's json.dumps(). For example: jq -n --arg region "us-east-1" '{"region":$region}'. The variable names in the JSON file must match the variable declarations in your .tf files. Terraform also auto-loads any file matching *.auto.tfvars.json. You can pass additional variable files explicitly with -var-file="custom.tfvars.json". Never commit sensitive values (passwords, tokens, API keys) to a JSON variable file — use TF_VAR_name environment variables instead, which Terraform reads automatically and which can be injected by your CI/CD system without touching the file system.
What is the difference between jsonencode() and aws_iam_policy_document?
aws_iam_policy_document is an HCL data source that generates IAM-specific JSON using typed blocks (statement, principals, actions) and validates the structure at plan time. jsonencode() is a general-purpose JSON string builder — more flexible but with no IAM-specific validation. AWS recommends aws_iam_policy_document for maintainability: it enforces correct IAM JSON structure, supports source_policy_documents for policy merging, and produces a canonical JSON string that avoids unnecessary plan diffs. jsonencode() works fine for simple policies and for non-IAM JSON strings. The key difference: aws_iam_policy_document catches structurally invalid policies during terraform plan, while jsonencode() passes any valid HCL object through without IAM-level validation. See also AWS IAM policy JSON for more on IAM policy structure.
How do I reference a Terraform variable inside jsonencode()?
Use the variable directly inside the HCL object passed to jsonencode(): for example, jsonencode({"Resource": var.bucket_arn}). Terraform evaluates var.bucket_arn before serializing to JSON. For string interpolation within a JSON value, use the standard HCL template syntax inside the object: jsonencode({"Resource": "${var.bucket_arn}/*"}). You can reference locals, data sources, and resource attributes the same way — any valid Terraform expression works inside the object. For lists, pass a Terraform list: jsonencode({"Actions": var.allowed_actions}) where var.allowed_actions is list(string). jsonencode() handles all JSON escaping automatically, so you never need to manually escape quotes or backslashes in the values. Use REST API JSON patterns as a reference for structuring JSON payloads in Terraform-managed API gateway resources.
Why is my Terraform JSON policy getting escaped incorrectly?
If you are passing a jsonencode() result into a resource that expects a JSON string (like aws_iam_role_policy.policy), the output of jsonencode() is already a properly escaped JSON string. Do not wrap it in another jsonencode() call or tostring(). Double-encoding produces {"key":"val"} with escaped quotes instead of the correct string. This is the most common cause of "invalid JSON" errors in Terraform-managed IAM policies. The fix is to use jsonencode(local.policy_object) exactly once. Another common mistake is using jsonencode() on a string variable that already contains JSON — jsonencode("string") produces a JSON-encoded string literal with extra surrounding quotes. Pass the decoded object, not a JSON string, to jsonencode(). Use Jsonic's JSON formatter to verify the exact output string when debugging escaped policies.
Ready to work with Terraform JSON policies?
Use Jsonic's JSON Formatter to validate and pretty-print IAM policy JSON from your Terraform modules. You can also diff two JSON policies to compare before/after changes when refactoring IAM permissions.
Open JSON Formatter