caddyserver/caddy · error

config is not valid JSON: %w; did you mean to use a config a

Error message

config is not valid JSON: %w; did you mean to use a config adapter (the --adapter flag)?

What it means

Same JSON validation gate as the offset variant, but the unmarshal error was not a *json.SyntaxError — typically *json.UnmarshalTypeError (right syntax, wrong type, e.g. a string where an object is required) or unexpected EOF on a truncated file. The --adapter hint is still appended because non-JSON input is a plausible cause.

Source

Thrown at cmd/main.go:237

		for _, warn := range warnings {
			msg := warn.Message
			if warn.Directive != "" {
				msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
			}
			logger.Warn(msg,
				zap.String("adapter", adapterName),
				zap.String("file", warn.File),
				zap.Int("line", warn.Line))
		}
		config = adaptedConfig
	} else if len(config) != 0 {
		// validate that the config is at least valid JSON
		err = json.Unmarshal(config, new(any))
		if err != nil {
			if jsonErr, ok := err.(*json.SyntaxError); ok {
				return nil, "", "", fmt.Errorf("config is not valid JSON: %w, at offset %d; did you mean to use a config adapter (the --adapter flag)?", err, jsonErr.Offset)
			}
			return nil, "", "", fmt.Errorf("config is not valid JSON: %w; did you mean to use a config adapter (the --adapter flag)?", err)
		}
	}

	return config, configFile, adapterName, nil
}

// watchConfigFile watches the config file at filename for changes
// and reloads the config if the file was updated. This function
// blocks indefinitely; it only quits if the poller has errors for
// long enough time. The filename passed in must be the actual
// config file used, not one to be discovered.
// Each second the config files is loaded and parsed into an object
// and is compared to the last config object that was loaded
func watchConfigFile(filename, adapterName string) {
	defer func() {
		if err := recover(); err != nil {
			log.Printf("[PANIC] watching config file: %v\n%s", err, debug.Stack())
		}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Check the type at the reported field — the error names the Go type expected
  2. Validate structure with jq and compare against the JSON config schema in the docs
  3. If the input is not JSON at all, pass --adapter
  4. Regenerate the file if it may be truncated (compare size against the producer's expectation)

Example fix

// before
{ "admin": "localhost:2019" }

// after
{ "admin": { "listen": "localhost:2019" } }
Defensive patterns

Strategy: validation

Validate before calling

// Type-check the top-level shape before passing the config along:
var probe map[string]json.RawMessage
if err := json.Unmarshal(raw, &probe); err != nil { return fmt.Errorf("config JSON invalid: %w", err) }
if a, ok := probe["admin"]; ok {
    var admin caddy.AdminConfig
    if err := json.Unmarshal(a, &admin); err != nil { return fmt.Errorf("admin field has wrong type: %w", err) }
}

Prevention

When it happens

Trigger: A JSON config where a field has the wrong shape ("admin": "localhost:2019" instead of an object); a truncated file so the parser hits EOF unexpectedly; Unicode issues producing a non-SyntaxError failure.

Common situations: Hand-writing JSON configs and giving a scalar where Caddy expects an object/array; disk-full writes truncating the config.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/e48e6a571e7842c2. Report an issue: GitHub.