hashicorp/terraform · error

Can't serialize backend configuration as JSON: %s

Error message

Can't serialize backend configuration as JSON: %s

What it means

Thrown during `terraform state migrate` when capturing the destination backend config into the BackendStateFile: bsf.Backend.SetConfig(dstConfig, dstB.ConfigSchema()) fails. SetConfig marshals the cty config value to JSON via ctyjson.Marshal against the backend schema's ImpliedType; it errors when the value contains attributes/types not present in that schema. The %s is the underlying marshal error.

Source

Thrown at internal/command/state_migrate.go:194

			migrateOpts.DestinationType = rootMod.Backend.Type
			migrateOpts.Destination = dstB

			// Capture details of the destination backend for updating the backend state file after a successful migration.
			_, cHash, bcDiags := c.backendConfig(&BackendOpts{
				BackendConfig: rootMod.Backend,
			})
			diags = diags.Append(bcDiags)
			if bcDiags.HasErrors() {
				view.Diagnostics(diags)
				return 1
			}
			bsf.Backend = &workdir.BackendConfigState{
				Type: rootMod.Backend.Type,
				Hash: uint64(cHash),
			}
			err := bsf.Backend.SetConfig(dstConfig, dstB.ConfigSchema())
			if err != nil {
				diags = diags.Append(fmt.Errorf("Can't serialize backend configuration as JSON: %s", err))
				view.Diagnostics(diags)
				return 1
			}
		}
	} else if rootMod.StateStore != nil {
		// Get single required_providers entry for state store provider.
		dstReq, dstReqDiags := c.getDestinationStateStoreProviderRequirements(rootMod.StateStore.ProviderAddr, rootMod.ProviderRequirements)
		diags = diags.Append(dstReqDiags)
		if dstReqDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}

		// Load any pre-existing destination provider lock file.
		var lockfilePath string
		if args.DestinationLockFilePath != "" {
			lockfilePath = args.DestinationLockFilePath
		} else {

View on GitHub (pinned to c9def3e214)

Solutions

  1. Inspect the wrapped %s message: it names the exact attribute/type that failed to marshal against the backend schema.
  2. Remove any backend-block attributes not accepted by the destination backend type (compare against that backend's docs).
  3. Re-run `terraform init` for the destination backend so its schema is current.
  4. If migrating backend types, edit the backend block to match the destination backend's required/optional attributes first.

Example fix

# before - migrating to s3 with a cloud-only attribute
terraform {
  backend "s3" {
    workspaces { name = "dev" } # not a valid s3 attribute
  }
}

# after
terraform {
  backend "s3" {
    bucket = "tf-state"
    key    = "dev/terraform.tfstate"
    region = "us-east-1"
  }
}
Defensive patterns

Strategy: validation

Validate before calling

// validate a backend block against its schema before migrating.
// Pseudocode: marshal the config value and unmarshal against the
// destination backend's ConfigSchema() implied type.
func validateBackendConfig(val cty.Value, schema *configschema.Block) error {
    _, err := ctyjson.Marshal(val, schema.ImpliedType())
    return err
}

Prevention

When it happens

Trigger: Running `terraform state migrate` to a destination backend whose in-memory configuration value (dstConfig from backendInitFromConfig) does not match the schema returned by dstB.ConfigSchema(). Typically a backend schema/version mismatch, a malformed backend block, or a provider/backend that changed its schema between when the config was parsed and when SetConfig ran.

Common situations: Migrating between backend types (s3 -> azurerm, local -> cloud) with extra/unknown attributes; a backend plugin shipped a schema change; leftover deprecated attributes in the backend block; custom backend that advertises a narrower schema than the config supplies.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/9ff16ebf82069d15. Report an issue: GitHub.