conform

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2025 License: MIT Imports: 15 Imported by: 0

README ΒΆ

Conform - Declarative Configuration & Validation for Go

Go Version License GitHub CI/CD Go Reference

The Pydantic for Go - Type-safe configuration loading, validation, and management in one elegant package.

Features β€’ Quick Start β€’ Documentation β€’ Examples


🎯 Why Conform?

Stop juggling multiple libraries. Conform unifies configuration loading, type conversion, and validation into a single, declarative interface.

Before Conform ❌
// Load config
viper.SetConfigFile("config.yaml")
viper.ReadInConfig()

// Unmarshal
var cfg Config
viper.Unmarshal(&cfg)

// Validate
validate := validator.New()
if err := validate.Struct(cfg); err != nil {
    // Parse errors...
}

// Type conversion? Manual!
port, _ := strconv.Atoi(viper.GetString("port"))
timeout, _ := time.ParseDuration(viper.GetString("timeout"))
With Conform βœ…
type Config struct {
    Port     int           `conform:"env=PORT,default=8080,validate=gte:1024"`
    Timeout  time.Duration `conform:"env=TIMEOUT,default=30s"`
    Database string        `conform:"env=DB_URL,required,validate=url"`
}

cfg, err := conform.LoadGeneric[Config](conform.FromEnv())
// ✨ Done! Type-safe, validated, ready to use.

One struct tag. One function call. Zero boilerplate.


✨ Features

🎯 Core Features
  • 🏷️ Declarative Configuration - Everything in struct tags, zero boilerplate
  • πŸš€ Type-Safe Generics - Full type safety with Go 1.21+ generics
  • πŸ”„ Multi-Source Support - Environment variables, files (YAML/JSON/TOML), Consul, custom sources
  • βœ… Built-in Validation - 25+ validators out of the box (email, URL, IP, numeric ranges, regex, etc.)
  • πŸ”§ Smart Type Coercion - Automatic conversion for bool, time.Duration, []int, time.Time, map[string]int
  • πŸ“¦ Nested Structs - Full support with automatic prefix handling
  • πŸ”₯ Hot Reload - Watch for changes and reload automatically
  • πŸ’¬ Beautiful Errors - Detailed error messages with field paths, suggestions, and locations
  • 🌍 Environment-Specific - Load different configs for dev/staging/prod
  • πŸ” Variable Substitution - ${VAR_NAME:-default} syntax in config values and file paths
🎨 What Makes Conform Different?
Feature Conform Viper envconfig go-config
Type Safety βœ… Generics ❌ ❌ ❌
Validation βœ… Built-in ❌ ❌ ⚠️ Limited
Error Messages βœ… Beautiful ❌ ❌ ⚠️ Basic
Hot Reload βœ… Built-in βœ… ❌ ❌
Environment-Specific βœ… Built-in ⚠️ Manual ❌ ❌
Variable Substitution βœ… Built-in ❌ ❌ ❌
Declarative βœ… 100% ⚠️ Partial βœ… ⚠️ Partial
Zero Boilerplate βœ… ❌ ⚠️ ⚠️

πŸ“¦ Installation

go get github.com/alicanli1995/conform

πŸš€ Quick Start

Basic Example
package main

import (
    "fmt"
    "os"
    
    "github.com/alicanli1995/conform"
)

type Config struct {
    Port     int    `conform:"env=APP_PORT,default=8080,validate=gte:1024"`
    Host     string `conform:"env=APP_HOST,default=localhost,validate=hostname"`
    Database string `conform:"env=DATABASE_URL,required,validate=url"`
}

func main() {
    os.Setenv("APP_PORT", "3000")
    os.Setenv("DATABASE_URL", "postgres://localhost/mydb")
    
    cfg, err := conform.LoadGeneric[Config](conform.FromEnv())
    if err != nil {
        panic(err) // Beautiful error messages!
    }
    
    fmt.Printf("Server: %s:%d\n", cfg.Host, cfg.Port)
}

πŸ“– Documentation

Multi-Source Support

Load from multiple sources with automatic priority (first source wins):

cfg, err := conform.LoadGeneric[Config](
    conform.FromEnv(),                    // Highest priority
    conform.FromFile("secrets.json"),     // Second priority
    conform.FromFile("config.yaml"),      // Third priority
    conform.FromConsul("service/config"), // Fourth priority
)
// Priority: env > consul > json > yaml > defaults
Environment-Specific Configuration

Perfect for dev/staging/production environments:

type Config struct {
    Database struct {
        Host string `conform:"file=database.host,default=localhost"`
        Port int    `conform:"file=database.port,default=5432"`
    }
}

// Development
devCfg, _ := conform.LoadGeneric[Config](
    conform.WithEnvironment("development"),
    conform.FromFile("config.${ENV}.yaml"), // Loads config.development.yaml
)

// Production
prodCfg, _ := conform.LoadGeneric[Config](
    conform.WithEnvironment("production"),
    conform.FromFile("config.${ENV}.yaml"), // Loads config.production.yaml
)
Variable Substitution

Use ${VAR_NAME:-default} syntax in config values:

type Config struct {
    DatabaseURL string `conform:"env=DB_URL,default=postgres://${DB_USER:-postgres}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}/${DB_NAME:-mydb}"`
    APIURL      string `conform:"env=API_URL,default=https://api.${ENV:-dev}.example.com"`
}
Smart Type Coercion

Automatic conversion for complex types:

type Config struct {
    // String "true" β†’ bool true
    Debug bool `conform:"env=DEBUG"`
    
    // String "30s" β†’ time.Duration
    Timeout time.Duration `conform:"env=TIMEOUT"`
    
    // String "1,2,3" β†’ []int{1,2,3}
    IDs []int `conform:"env=IDS,separator=,"`
    
    // String "1=one,2=two" β†’ map[int]string{1:"one", 2:"two"}
    Mapping map[int]string `conform:"env=MAPPING"`
    
    // String "2024-01-01" β†’ time.Time
    StartDate time.Time `conform:"env=START,format=2006-01-02"`
}
Beautiful Error Messages

Get detailed, actionable error messages:

cfg, err := conform.LoadGeneric[Config](conform.FromEnv())
if err != nil {
    fmt.Println(err)
    // Output:
    // ❌ Configuration validation failed:
    //
    // 1. Port (APP_PORT): value 80 is too small
    //    Got: 80
    //    Location: env var APP_PORT
    //    πŸ’‘ Suggestion: Use a value >= 1024 (e.g. 8080)
    //
    // 2. Database.URL (DB_URL): invalid URL format
    //    Got: "not-a-url"
    //    Expected: valid URL with scheme
    //    πŸ’‘ Suggestion: Format should be: https://example.com
}
Hot Reload

Watch for configuration changes automatically:

watcher, err := conform.Watch[Config](func(newCfg Config) {
    log.Printf("Config reloaded: %+v", newCfg)
    // Update your application state here
}, conform.FromEnv(), conform.FromFile("config.yaml"))

// Thread-safe access
cfg := watcher.Get()

// Stop watching
defer watcher.Stop()
Custom Validators

Register your own validation rules:

conform.RegisterValidator("strong_password", func(val interface{}, params []string) error {
    str := val.(string)
    if len(str) < 12 {
        return fmt.Errorf("password must be at least 12 characters")
    }
    if !hasSpecialChar(str) {
        return fmt.Errorf("password must contain special character")
    }
    return nil
})

type Config struct {
    Password string `conform:"env=PASSWORD,validate=strong_password"`
}
Custom Converters

Convert to custom types:

type CustomType string

conform.RegisterConverter(
    reflect.TypeOf(CustomType("")),
    func(s string) (interface{}, error) {
        return CustomType("custom_" + s), nil
    },
)

type Config struct {
    Custom CustomType `conform:"env=CUSTOM"`
}
Nested Configuration

Full support for nested structs with automatic prefix handling:

type DatabaseConfig struct {
    Host string `conform:"env=HOST,default=localhost"`
    Port int    `conform:"env=PORT,default=5432"`
}

type AppConfig struct {
    Name     string         `conform:"env=APP_NAME,default=MyApp"`
    Database DatabaseConfig `conform:"prefix=DB_"`
}

// Environment variables:
// DB_HOST=db.example.com
// DB_PORT=5432
File Configuration

Support for YAML, JSON, and TOML:

type Config struct {
    Server struct {
        Host string `conform:"file=server.host,default=localhost"`
        Port int    `conform:"file=server.port,default=8080"`
    }
}

// YAML
cfg, _ := conform.LoadGeneric[Config](conform.FromFile("config.yaml"))

// TOML
cfg, _ := conform.LoadGeneric[Config](conform.FromFile("config.toml"))

// JSON
cfg, _ := conform.LoadGeneric[Config](conform.FromFile("config.json"))

πŸ“š Tag Reference

Source Tags
Tag Description Example
env=VAR_NAME Load from environment variable env=APP_PORT
file=key.path Load from config file (dot notation) file=database.host
default=value Default value if not found default=8080
required Field is required (error if missing) required
prefix=PREFIX_ Prefix for nested structs prefix=DB_
Type Conversion Tags
Tag Description Example
format=layout Format for time.Time format=2006-01-02
separator=, Separator for slices separator=|
Validation Tags
Tag Description Example
validate=rule:param Validation rules validate=gte:1024,lte:65535

βœ… Built-in Validators

Numeric Validators
  • min:value - Minimum value/length
  • max:value - Maximum value/length
  • gte:value - Greater than or equal
  • lte:value - Less than or equal
  • eq:value - Equal to
  • ne:value - Not equal to
String Validators
  • email - Valid email address
  • url - Valid URL (use url:https for HTTPS only)
  • ip - Valid IP address (IPv4 or IPv6)
  • hostname - Valid hostname
  • alphanum - Only letters and digits
  • alpha - Only letters
  • numeric - Only digits
  • regex:pattern - Match regex pattern
  • oneof:val1:val2 - One of the specified values
  • len:length - Exact length
Password Validators
  • has_upper - Contains uppercase letter
  • has_lower - Contains lowercase letter
  • has_digit - Contains digit
  • has_special - Contains special character
General
  • required - Field is required

πŸ”§ Advanced Usage

Multiple File Sources
cfg, err := conform.LoadGeneric[Config](
    conform.FromFile("config.yaml"),
    conform.FromFile("secrets.yaml"),
    conform.FromEnv(),
)
Custom Sources
type CustomSource struct{}

func (c *CustomSource) Get(key string) (string, bool) {
    // Your implementation
    return value, found
}

cfg, err := conform.LoadGeneric[Config](
    conform.WithSource(&CustomSource{}),
)
Slices and Arrays
type Config struct {
    Hosts []string `conform:"env=HOSTS,separator=|"`
    Ports []int    `conform:"env=PORTS,separator=,"`
}

// HOSTS=host1|host2|host3
// PORTS=8080,8081,8082
Maps
type Config struct {
    // String "key1=value1,key2=value2" β†’ map[string]string
    StringMap map[string]string `conform:"env=STRING_MAP"`
    
    // String "1=one,2=two" β†’ map[int]string
    IntMap map[int]string `conform:"env=INT_MAP"`
}
Time Parsing
type Config struct {
    StartTime time.Time `conform:"env=START_TIME,format=2006-01-02T15:04:05Z"`
    BirthDate time.Time `conform:"env=BIRTH_DATE,format=2006-01-02"`
}

// START_TIME=2024-01-01T00:00:00Z
// BIRTH_DATE=1990-05-15

πŸ”§ CLI Tool

Validate configuration files without running your application:

# Install
go install github.com/alicanli1995/conform/cmd/conform-validate@latest

# Validate a config file
conform-validate -config=config.yaml -struct=config.go -struct-name=Config

πŸ“š Examples

Comprehensive examples available in the examples directory:


πŸ—οΈ Architecture

Conform consists of four main components:

  1. Parser - Parses struct tags and extracts configuration metadata
  2. Converter - Converts string values to typed Go values
  3. Validator - Validates values against declarative rules
  4. Loader - Orchestrates loading from multiple sources with priority

πŸ“„ License

MIT License - see LICENSE file for details.


Made with ❀️ for the Go community

⭐ Star us on GitHub β€’ πŸ“¦ pkg.go.dev β€’ πŸ“– Documentation β€’ πŸ’¬ Issues

Documentation ΒΆ

Index ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

This section is empty.

Functions ΒΆ

func Load ΒΆ

func Load(target interface{}, opts ...Option) error

Load loads configuration into the target struct (non-generic version for backward compatibility)

func LoadGeneric ΒΆ

func LoadGeneric[T any](opts ...Option) (*T, error)

LoadGeneric loads configuration using generics - returns the config struct directly

func RegisterConverter ΒΆ

func RegisterConverter(t reflect.Type, fn convert.ConvertFunc)

RegisterConverter registers a global converter

func RegisterValidator ΒΆ

func RegisterValidator(name string, fn func(value interface{}, params []string) error)

RegisterValidator registers a global validator

func TestLoadGeneric_EnvironmentSpecific ΒΆ

func TestLoadGeneric_EnvironmentSpecific(t *testing.T)

func TestLoadGeneric_VariableSubstitution ΒΆ

func TestLoadGeneric_VariableSubstitution(t *testing.T)

func TestLoadGeneric_VariableSubstitutionWithDefaults ΒΆ

func TestLoadGeneric_VariableSubstitutionWithDefaults(t *testing.T)

Types ΒΆ

type Config ΒΆ

type Config struct {
	Source      source.Source
	FileLoader  *loader.FileLoader
	Converter   *convert.Converter
	Validator   *validate.Validator
	FileSources []source.Source
	Sources     []source.Source
	Environment string            // Environment name (dev, staging, production, etc.)
	Variables   map[string]string // Custom variables for substitution
}

Config holds the configuration for loading

type ErrorList ΒΆ

type ErrorList struct {
	Errors []FieldError
}

ErrorList collects multiple field errors

func (*ErrorList) Error ΒΆ

func (e *ErrorList) Error() string

type FieldError ΒΆ

type FieldError struct {
	Field      string
	FieldPath  string
	Value      interface{}
	Message    string
	Key        string // env var or file key
	Location   string // where the error occurred
	Suggestion string
}

FieldError represents an error for a specific field

type Option ΒΆ

type Option func(*Config)

Option is a configuration option

func FromConsul ΒΆ

func FromConsul(path string) Option

FromConsul creates an option that loads from Consul (placeholder - would need consul client)

func FromEnv ΒΆ

func FromEnv() Option

FromEnv creates an option that loads from environment variables

func FromFile ΒΆ

func FromFile(filePath string) Option

FromFile creates an option that loads from a file Supports environment-specific files: config.${ENV}.yaml

func WithConverter ΒΆ

func WithConverter(conv *convert.Converter) Option

WithConverter sets a custom converter

func WithEnvironment ΒΆ

func WithEnvironment(env string) Option

WithEnvironment sets the environment name for environment-specific configs Example: WithEnvironment("production") will load config.production.yaml

func WithFile ΒΆ

func WithFile(filePath string) Option

WithFile sets a config file path (backward compatibility alias for FromFile)

func WithFileLoader ΒΆ

func WithFileLoader(fileLoader *loader.FileLoader) Option

WithFileLoader sets a file loader

func WithSource ΒΆ

func WithSource(s source.Source) Option

WithSource sets a custom source

func WithValidator ΒΆ

func WithValidator(v *validate.Validator) Option

WithValidator sets a custom validator

func WithVariables ΒΆ

func WithVariables(vars map[string]string) Option

WithVariables sets custom variables for substitution in config values Example: WithVariables(map[string]string{"APP_NAME": "MyApp"})

type Watcher ΒΆ

type Watcher[T any] struct {
	// contains filtered or unexported fields
}

Watcher watches for configuration changes and reloads automatically

func Watch ΒΆ

func Watch[T any](onReload func(T), opts ...Option) (*Watcher[T], error)

Watch creates a watcher that monitors configuration changes

func (*Watcher[T]) Get ΒΆ

func (w *Watcher[T]) Get() T

Get returns the current configuration (thread-safe)

func (*Watcher[T]) Stop ΒΆ

func (w *Watcher[T]) Stop()

Stop stops watching for changes

Directories ΒΆ

Path Synopsis
cmd
examples
advanced command
basic command
custom command
customvalidator command
environment command
errors command
fileconfig command
generic command
hotreload command
realworld command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL