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:
-
Parsing: The
Parsefunction tokenizes the Caddyfile contents into server blocks and directives, creating an abstract syntax tree. -
Setup: The
ServerType.Setupmethod 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. -
Marshaling: The resulting
*caddy.Configstructure is serialized to JSON usingjson.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
Adapterinterface incaddyconfig/configadapters.godefines the contract for configuration conversion through theAdaptmethod. - Global Registry: The
configAdaptersmap andRegisterAdapterfunction enable dynamic discovery of adapters, whileGetAdapterfacilitates 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
Adapterinterface and register implementations duringinit()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →