Custom Codecs
7 minute read
The config package allows you to extend configuration support to any format by implementing and registering custom codecs.
Encoder and Decoder
Registration uses codec.Type as the name. A format codec is usually one type that implements both codec.Encoder and codec.Decoder:
type Encoder interface {
Encode(v any) ([]byte, error)
}
type Decoder interface {
Decode(data []byte, v any) error
}
Encode turns a value into bytes; Decode fills a value (often *map[string]any) from bytes.
Built-in Codecs
The config package includes several built-in codecs.
Format Codecs
| Codec | Type | Capabilities |
|---|---|---|
| JSON | codec.TypeJSON | Encode & Decode |
| YAML | codec.TypeYAML | Encode & Decode |
| TOML | codec.TypeTOML | Encode & Decode |
| EnvVar | codec.TypeEnvVar | Decode only |
Caster Codecs
Caster codecs handle type conversion.
| Codec | Type | Converts To |
|---|---|---|
| Bool | codec.TypeCasterBool | bool |
| Int | codec.TypeCasterInt | int |
| Int8/16/32/64 | codec.TypeCasterInt8, etc. | int8, int16, etc. |
| Uint | codec.TypeCasterUint | uint |
| Uint8/16/32/64 | codec.TypeCasterUint8, etc. | uint8, uint16, etc. |
| Float32/64 | codec.TypeCasterFloat32, codec.TypeCasterFloat64 | float32, float64 |
| String | codec.TypeCasterString | string |
| Time | codec.TypeCasterTime | time.Time |
| Duration | codec.TypeCasterDuration | time.Duration |
Implementing a Custom Codec
Basic Example: INI Format
Let’s implement a simple INI file codec.
package inicodec
import (
"bufio"
"bytes"
"fmt"
"strings"
"rivaas.dev/config/codec"
)
type INICodec struct{}
func (c INICodec) Decode(data []byte, v any) error {
result := make(map[string]any)
scanner := bufio.NewScanner(bytes.NewReader(data))
var currentSection string
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
// Section header
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
currentSection = strings.Trim(line, "[]")
if result[currentSection] == nil {
result[currentSection] = make(map[string]any)
}
continue
}
// Key-value pair
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if currentSection != "" {
section := result[currentSection].(map[string]any)
section[key] = value
} else {
result[key] = value
}
}
// Type assertion to set result
target := v.(*map[string]any)
*target = result
return scanner.Err()
}
func (c INICodec) Encode(v any) ([]byte, error) {
data, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map[string]any, got %T", v)
}
var buf bytes.Buffer
for section, values := range data {
sectionMap, ok := values.(map[string]any)
if !ok {
// Top-level key-value
buf.WriteString(fmt.Sprintf("%s = %v\n", section, values))
continue
}
// Section header
buf.WriteString(fmt.Sprintf("[%s]\n", section))
// Section key-values
for key, value := range sectionMap {
buf.WriteString(fmt.Sprintf("%s = %v\n", key, value))
}
buf.WriteString("\n")
}
return buf.Bytes(), nil
}
func init() {
const iniType codec.Type = "ini"
codec.RegisterEncoder(iniType, INICodec{})
codec.RegisterDecoder(iniType, INICodec{})
}
Using the Custom Codec
package main
import (
"context"
"log"
"rivaas.dev/config"
"rivaas.dev/config/codec"
_ "yourmodule/inicodec" // Register codec via init()
)
func main() {
cfg := config.MustNew(
config.WithFileAs("config.ini", codec.Type("ini")),
)
if err := cfg.Load(context.Background()); err != nil {
log.Fatalf("failed to load config: %v", err)
}
host := cfg.String("server.host")
port := cfg.Int("server.port")
log.Printf("Server: %s:%d", host, port)
}
config.ini:
[server]
host = localhost
port = 8080
[database]
host = db.example.com
port = 5432
Registering Codecs
Codecs must be registered before use:
import "rivaas.dev/config/codec"
func init() {
t := codec.Type("mytype")
codec.RegisterEncoder(t, MyCodec{})
codec.RegisterDecoder(t, MyCodec{})
}
Registration functions:
RegisterEncoder(name codec.Type, encoder codec.Encoder)— register an encoderRegisterDecoder(name codec.Type, decoder codec.Decoder)— register a decoder
You can register the same codec for both or different codecs for each operation.
Decode-Only Codecs
Some codecs only support decoding (like the built-in EnvVar codec):
type EnvVarCodec struct{}
func (c EnvVarCodec) Decode(data []byte, v any) error {
// Decode environment variable format
// ...
}
func (c EnvVarCodec) Encode(v any) ([]byte, error) {
return nil, errors.New("encoding to environment variables not supported")
}
func init() {
codec.RegisterDecoder(codec.Type("envvar"), EnvVarCodec{})
// Note: not registering an encoder
}
Advanced Example: XML Codec
A more complete example with error handling:
package xmlcodec
import (
"encoding/xml"
"fmt"
"rivaas.dev/config/codec"
)
type XMLCodec struct{}
func (c XMLCodec) Decode(data []byte, v any) error {
target, ok := v.(*map[string]any)
if !ok {
return fmt.Errorf("expected *map[string]any, got %T", v)
}
// XML unmarshaling to intermediate structure
var intermediate struct {
XMLName xml.Name
Content []byte `xml:",innerxml"`
}
if err := xml.Unmarshal(data, &intermediate); err != nil {
return fmt.Errorf("xml decode error: %w", err)
}
// Convert XML to map structure
result := make(map[string]any)
// ... conversion logic ...
*target = result
return nil
}
func (c XMLCodec) Encode(v any) ([]byte, error) {
data, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map[string]any, got %T", v)
}
// Convert map to XML structure
xmlData, err := xml.MarshalIndent(data, "", " ")
if err != nil {
return nil, fmt.Errorf("xml encode error: %w", err)
}
return xmlData, nil
}
func init() {
xmlType := codec.Type("xml")
codec.RegisterEncoder(xmlType, XMLCodec{})
codec.RegisterDecoder(xmlType, XMLCodec{})
}
Caster Codecs
Built-in caster types are registered as decoders on the codec registry. Config getters (Int, Duration, and so on) use spf13/cast on merged values, not codec.GetDecoder(TypeCaster…) on each read.
port := cfg.Int("server.port")
timeout := cfg.Duration("timeout")
Custom Caster Example
If you need custom type conversion:
type URLCaster struct{}
func (c URLCaster) Decode(data []byte, v any) error {
target, ok := v.(*url.URL)
if !ok {
return fmt.Errorf("expected *url.URL, got %T", v)
}
parsedURL, err := url.Parse(string(data))
if err != nil {
return fmt.Errorf("invalid URL: %w", err)
}
*target = *parsedURL
return nil
}
func (c URLCaster) Encode(v any) ([]byte, error) {
u, ok := v.(*url.URL)
if !ok {
return nil, fmt.Errorf("expected *url.URL, got %T", v)
}
return []byte(u.String()), nil
}
When to Create Custom Codecs
Use Custom Codecs For:
- Unsupported formats - XML, INI, HCL, proprietary formats
- Legacy formats - Converting old configuration formats
- Encrypted configurations - Decrypting config data
- Compressed data - Handling gzip/compressed configs
- Custom protocols - Special encoding schemes
Use Built-in Codecs For:
- Standard formats - JSON, YAML, TOML
- Type conversion on read — use typed getters; nested structures use struct binding
- Simple text formats - Can often use JSON/YAML
Best Practices
1. Error Handling
Provide clear error messages:
func (c MyCodec) Decode(data []byte, v any) error {
if len(data) == 0 {
return errors.New("empty data")
}
target, ok := v.(*map[string]any)
if !ok {
return fmt.Errorf("expected *map[string]any, got %T", v)
}
// ... decoding logic ...
if err != nil {
return fmt.Errorf("decode error at line %d: %w", line, err)
}
return nil
}
2. Type Validation
Validate expected types:
func (c MyCodec) Decode(data []byte, v any) error {
target, ok := v.(*map[string]any)
if !ok {
return fmt.Errorf("MyCodec requires *map[string]any, got %T", v)
}
// ...
}
3. Use init() for Registration
Register codecs in init() for automatic setup:
func init() {
t := codec.Type("myformat")
codec.RegisterEncoder(t, MyCodec{})
codec.RegisterDecoder(t, MyCodec{})
}
4. Thread Safety
Ensure your codec is thread-safe:
type MyCodec struct {
// No mutable state
}
// OR use proper synchronization
type MyCodec struct {
mu sync.Mutex
cache map[string]any
}
5. Document Your Codec
Include usage examples:
// MyCodec implements encoding/decoding for the XYZ format.
//
// Example usage:
//
// import _ "yourmodule/mycodec"
//
// cfg := config.MustNew(
// config.WithFileAs("config.xyz", codec.Type("xyz")),
// )
//
type MyCodec struct{}
Complete Example
package main
import (
"context"
"log"
"rivaas.dev/config"
"rivaas.dev/config/codec"
_ "yourmodule/xmlcodec" // Custom XML codec
_ "yourmodule/inicodec" // Custom INI codec
)
func main() {
cfg := config.MustNew(
config.WithFile("config.yaml"), // Built-in YAML
config.WithFileAs("config.xml", codec.Type("xml")), // Custom XML
config.WithFileAs("config.ini", codec.Type("ini")), // custom INI
config.WithEnv("MYAPP_"), // Built-in EnvVar
)
if err := cfg.Load(context.Background()); err != nil {
log.Fatalf("failed to load config: %v", err)
}
port := cfg.Int("server.port")
host := cfg.String("server.host")
log.Printf("Server: %s:%d", host, port)
}
Testing Custom Codecs
Write tests for your codecs:
func TestMyCodec_Decode(t *testing.T) {
codec := MyCodec{}
input := []byte(`
[server]
host = localhost
port = 8080
`)
var result map[string]any
err := subject.Decode(input, &result)
assert.NoError(t, err)
assert.Equal(t, "localhost", result["server"].(map[string]any)["host"])
assert.Equal(t, "8080", result["server"].(map[string]any)["port"])
}
func TestMyCodec_Encode(t *testing.T) {
codec := MyCodec{}
data := map[string]any{
"server": map[string]any{
"host": "localhost",
"port": 8080,
},
}
output, err := subject.Encode(data)
assert.NoError(t, err)
assert.Contains(t, string(output), "[server]")
assert.Contains(t, string(output), "host = localhost")
}
Next Steps
- See Examples for real-world codec usage
- Review Codecs Reference for technical details
- Explore Multiple Sources for combining custom formats
Tip: If you create a useful codec, consider contributing it to the community!
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.