hashicorp/terraform · error

Can't serialize backend configuration as JSON

Error message

Can't serialize backend configuration as JSON: %s

What it means

After locking, Terraform serializes the chosen backend config into the cache file via s.Backend.SetConfig(configVal, b.ConfigSchema()). The wrapped %s is the JSON marshal/encode error. This indicates the config value (cty) could not be encoded against the backend's declared schema, normally a schema/cty mismatch rather than a user input error.

Solutions

  1. Read the inner %s to identify which attribute/type failed to encode.
  2. Update the backend (built-in or plugin) to a version whose schema matches this Terraform build.
  3. Simplify the backend block to only documented, schema-declared attributes and remove experimental/extra fields.
  4. If reproducible with a stock backend on the latest release, file a bug with the inner error and the backend type/version.

Example fix

// before
backend "foo" {
  unknown_attr = "x"  # not in backend schema -> encode mismatch
}

// after
backend "foo" {
  # only schema-declared attributes
  region = "us-east-1"
}
Defensive patterns

Strategy: validation

Validate before calling

// Ensure configVal matches the backend schema before SetConfig.
func validateConfigAgainstSchema(configVal cty.Value, schema *configschema.Block) error {
    if schema == nil { return fmt.Errorf("backend schema is nil") }
    // use cty/json transform to attempt encode; surface mismatch early
    if _, err := json.Marshal(schema.ImpliedType().Value(configVal)); err != nil {
        return fmt.Errorf("config does not match backend schema: %w", err)
    }
    return nil
}

Try / catch

if err := s.Backend.SetConfig(configVal, b.ConfigSchema()); err != nil {
    if isJSONEncodingErr(err) {
        diags = diags.Append(fmt.Errorf("backend config does not match schema (remove undocumented attrs / align backend version): %s", err))
    } else {
        diags = diags.Append(fmt.Errorf("Can't serialize backend configuration as JSON: %s", err))
    }
    return nil, diags
}

Prevention

When it happens

Trigger: s.Backend.SetConfig fails marshalling configVal to JSON per b.ConfigSchema(). Triggers: backend ConfigSchema is nil or malformed, configVal contains a type the JSON encoder rejects (e.g. an unhandled cty capsule type), or a version skew between the running Terraform and the backend implementation's schema.

Common situations: Using a third-party backend plugin whose schema is inconsistent with the config value produced; a Terraform build mismatch; very rarely, a config value with deeply nested/optional attributes the schema did not declare.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/f6eb9c3356be11c8. Report an issue: GitHub.

Appendix: source

Thrown at internal/command/meta_backend.go:1795

		if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil {
			diags = diags.Append(fmt.Errorf("Error locking state: %s", err))
			return nil, diags
		}
		defer stateLocker.Unlock()
	}

	// Store the metadata in our saved state location
	s := sMgr.State()
	if s == nil {
		s = workdir.NewBackendStateFile()
	}
	s.Backend = &workdir.BackendConfigState{
		Type: c.Type,
		Hash: uint64(cHash),
	}
	err := s.Backend.SetConfig(configVal, b.ConfigSchema())
	if err != nil {
		diags = diags.Append(fmt.Errorf("Can't serialize backend configuration as JSON: %s", err))
		return nil, diags
	}

	// Verify that selected workspace exists in the backend.
	if opts.Init && b != nil {
		err := m.selectWorkspace(b)
		if err != nil {
			diags = diags.Append(err)

			// FIXME: A compatibility oddity with the 'remote' backend.
			// As an awkward legacy UX, when the remote backend is configured and there
			// are no workspaces, the output to the user saying that there are none and
			// the user should create one with 'workspace new' takes the form of an
			// error message - even though it's happy path, expected behavior.
			//
			// Therefore, only return nil with errored diags for everything else, and
			// allow the remote backend to continue and write its configuration to state
			// even though no workspace is selected.

View on GitHub (pinned to d32a084675)