How Caddy's Config Adapter System Works: Converting Custom Formats to JSON

Caddy's config adapter system transforms arbitrary configuration formats—such as the Caddyfile or custom domain-specific languages—into Caddy's native JSON structure through a standardized Go interface and global registry.

The caddyserver/caddy repository implements a pluggable adapter architecture that decouples configuration syntax from the core server logic. This system allows users to write configs in the Caddyfile format or any custom format, while the server internally operates on a unified JSON representation.

The Adapter Interface and Core Concepts

At the heart of Caddy's config adapter system lies a simple Go interface defined in caddyconfig/configadapters.go. Any type implementing this interface can serve as a configuration adapter.

The Adapter interface requires a single method:

Adapt(body []byte, options map[string]any) ([]byte, []caddyconfig.Warning, error)

This method receives the raw configuration file contents as body, along with an options map for additional parameters such as file paths or CLI flags. It returns three values: the adapted JSON configuration as bytes, a slice of Warning structs for non-fatal issues, and an error if adaptation fails.

The Warning struct surfaces contextual issues without halting execution:

type Warning struct {
    File       string
    Line       int
    Directive  string
    Message    string
}

The Adapter Registration and Lookup Mechanism

Caddy maintains a global registry to manage available adapters. This registry, defined as configAdapters map[string]Adapter in caddyconfig/configadapters.go, maps string names to adapter implementations.

Registering Adapters

Packages implement adapters and register them during initialization using the RegisterAdapter function:

func RegisterAdapter(name string, adapter Adapter) {
    if configAdapters == nil {
        configAdapters = make(map[string]Adapter)
    }
    configAdapters[name] = adapter
    // Also registers as a Caddy module for introspection
    RegisterModule(adapterModule{name: name, adapter: adapter})
}

This function stores the adapter in the global map and simultaneously registers it as a Caddy module via adapterModule. This dual registration allows the adapter to appear in Caddy's module list and support hot-reloading capabilities.

Retrieving Adapters

When Caddy loads a configuration, it calls GetAdapter(name string) to retrieve the appropriate adapter from the registry:

func GetAdapter(name string) Adapter {
    return configAdapters[name]
}

The core loader uses this function when processing the --adapter CLI flag or when auto-detecting the configuration format based on file extensions.

How the Caddyfile Adapter Works: A Concrete Implementation

The Caddyfile adapter serves as the primary reference implementation for Caddy's config adapter system. Located in caddyconfig/caddyfile/adapter.go, this adapter converts the Caddyfile syntax into JSON configuration.

The adapter registers itself in caddyconfig/httpcaddyfile/httptype.go:

func init() {
    caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{})
}

The Adaptation Flow

When caddyfile.Adapter.Adapt executes, it follows a three-phase process:

  1. Parsing: The Parse function tokenizes the Caddyfile contents into server blocks and directives, creating an abstract syntax tree.

  2. Setup: The ServerType.Setup method processes these blocks, constructing HTTP server configurations, TLS settings, and other app-specific structures. This phase handles complex logic such as global options, named routes, and matcher definitions.

  3. Marshaling: The resulting *caddy.Config structure is serialized to JSON using json.Marshal, producing the final configuration bytes that Caddy's core consumes.

This implementation demonstrates how the abstract Adapter interface translates domain-specific syntax into Caddy's internal JSON representation.

Creating a Custom Config Adapter

Developers can extend Caddy's config adapter system by implementing the Adapter interface and registering the implementation during package initialization.

Implementation Structure

A custom adapter requires a type that implements the Adapt method:

package myadapter

import (
    "encoding/json"
    "fmt"
    
    "github.com/caddyserver/caddy/v2"
    "github.com/caddyserver/caddy/v2/caddyconfig"
)

type MyAdapter struct{}

func (a MyAdapter) Adapt(body []byte, opts map[string]any) ([]byte, []caddyconfig.Warning, error) {
    // Example: Validate that input is already valid JSON
    var cfg caddy.Config
    if err := json.Unmarshal(body, &cfg); err != nil {
        return nil, nil, fmt.Errorf("invalid JSON: %w", err)
    }
    
    // Re-marshal to ensure consistent formatting
    jsonBytes, err := json.Marshal(cfg)
    if err != nil {
        return nil, nil, err
    }
    
    return jsonBytes, nil, nil
}

func init() {
    caddyconfig.RegisterAdapter("myformat", MyAdapter{})
}

Usage

Once compiled into a Caddy build, users invoke the custom adapter via CLI:

caddy run --config config.myfmt --adapter myformat

Caddy retrieves "myformat" using GetAdapter, invokes Adapt, and loads the resulting JSON configuration.

Summary

Caddy's config adapter system provides a clean abstraction for converting arbitrary configuration formats into the server's native JSON structure:

  • Standardized Interface: The Adapter interface in caddyconfig/configadapters.go defines the contract for configuration conversion through the Adapt method.
  • Global Registry: The configAdapters map and RegisterAdapter function enable dynamic discovery of adapters, while GetAdapter facilitates runtime retrieval.
  • Reference Implementation: The Caddyfile adapter demonstrates the full adaptation pipeline: parsing tokens, setting up server configurations, and marshaling to JSON.
  • Extensibility: Third-party developers implement the Adapter interface and register implementations during init() to add support for custom configuration languages.

Frequently Asked Questions

What is the config adapter system in Caddy?

The config adapter system is a pluggable architecture that converts configuration files from various formats—such as the Caddyfile or custom domain-specific languages—into Caddy's native JSON configuration. It consists of a Go interface definition, a global registry for adapter lookup, and concrete implementations that handle the actual parsing and transformation logic.

How do I use a custom config adapter with Caddy?

To use a custom adapter, implement the caddyconfig.Adapter interface with an Adapt method that transforms your format into valid Caddy JSON, then register it using caddyconfig.RegisterAdapter in your package's init function. After compiling your code into the Caddy binary, specify your adapter via the --adapter flag when running caddy run or caddy start.

What is the difference between the Caddyfile and JSON configuration formats?

The Caddyfile is a human-friendly, indentation-based configuration syntax that gets converted to JSON through the built-in Caddyfile adapter, while JSON is Caddy's native internal format that provides explicit structure and full access to all configuration options. The Caddyfile adapter handles the transformation by parsing the file into tokens, setting up server blocks and directives, and marshaling the result to JSON.

Where is the config adapter interface defined in the Caddy source code?

The Adapter interface is defined in caddyconfig/configadapters.go within the github.com/caddyserver/caddy/v2 repository. This file also contains the Warning struct, the global configAdapters registry map, and the RegisterAdapter and GetAdapter functions that manage adapter lifecycle and discovery.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →