dynamic foundation for data
Convert between Go structs and maps with ease
The dd package provides bidirectional data binding between Go structs and map[string]any, enabling seamless integration with any data system. Since maps are foundational to all data structures, this facilitates integration with networks, databases, files, and APIs.
Quick Reference
Section titled “Quick Reference”1. Basic Binding - Hello World
Section titled “1. Basic Binding - Hello World”Convert struct to map and back
import "github.com/michaelquigley/df/dd"
// struct → mapuser := User{Name: "John", Age: 30}data, err := dd.Unbind(user)// data: map[string]any{"name": "John", "age": 30}
// map → struct (modern way)userData := map[string]any{"name": "Alice", "age": 25}user, err := dd.New[User](userData)
// map → struct (manual allocation)var user Usererr := dd.Bind(&user, userData)2. Struct Tags - Field Control
Section titled “2. Struct Tags - Field Control”Control field mapping and validation
type User struct { Name string `dd:"+required"` // required field Email string `dd:"email_address"` // custom field name Age int `dd:",+required"` // default name, required Password string `dd:",+secret"` // hidden in output Internal string `dd:"-"` // skip completely Active bool // uses snake_case: "active"}Tag Options:
dd:"custom_name"- custom field namedd:"+required"- field is requireddd:",+secret"- hidden in inspect outputdd:",+extra"- capture unmatched keys (map[string]any only)dd:"-"- exclude from binding- No tag = automatic snake_case conversion
2.5. Extra Fields - Capturing Unknown Data
Section titled “2.5. Extra Fields - Capturing Unknown Data”Capture unmatched keys from input data
type Config struct { Name string `dd:"name"` Extra map[string]any `dd:",+extra"`}
// Bind captures unknown fieldsdata := map[string]any{ "name": "myapp", "custom_key": "value", "version": "1.0",}
config, _ := dd.New[Config](data)// config.Name = "myapp"// config.Extra = map[string]any{"custom_key": "value", "version": "1.0"}
// Unbind merges extras backresult, _ := dd.Unbind(config)// result = map[string]any{"name": "myapp", "custom_key": "value", "version": "1.0"}Extra Field Rules:
- Tag:
dd:",+extra"marks the capture field - Type: Must be
map[string]any - Only one
+extrafield per struct - Nested structs capture their own extras independently
- Embedded structs share parent’s namespace
Merge()adds new extras to existing map- Field remains
nilwhen no unknown keys exist
Use Cases:
- Forward compatibility - preserve unknown fields from newer data versions
- Extension data - allow user-defined custom fields
- Configuration passthrough - forward extra config to subsystems
- Round-trip safety - preserve all data through bind/unbind cycles
3. Type Coercion - Automatic Conversion
Section titled “3. Type Coercion - Automatic Conversion”Automatic type conversion between compatible types
// Input data with different typesdata := map[string]any{ "port": "8080", // string → int "timeout": 30.5, // float → int "enabled": "true", // string → bool "duration": "5m", // string → time.Duration}
type Config struct { Port int `dd:"port"` Timeout int `dd:"timeout"` Enabled bool `dd:"enabled"` Tags []string `dd:"tags"` Duration time.Duration `dd:"duration"`}
config, err := dd.New[Config](data)// All fields converted automatically4. File I/O - Direct Persistence
Section titled “4. File I/O - Direct Persistence”Read/write JSON and YAML files directly
// From filesconfig, err := dd.BindFromJSON[Config]("config.json")config, err := dd.BindFromYAML[Config]("config.yaml")
// To fileserr := dd.UnbindToJSON(config, "output.json")err := dd.UnbindToYAML(config, "output.yaml")
// With formatting optionserr := dd.UnbindToJSONIndent(config, "pretty.json", "", " ")5. Nested Structures - Complex Data
Section titled “5. Nested Structures - Complex Data”Handle deeply nested data structures
type User struct { Name string `dd:"name"` Profile *Profile `dd:"profile"` // pointer to nested struct Tags []Tag `dd:"tags"` // slice of structs}
type Profile struct { Bio string `dd:"bio"` Website string `dd:"website"`}
type Tag struct { Name string `dd:"name"` Color string `dd:"color"`}
// Nested data automatically handleddata := map[string]any{ "name": "John", "profile": map[string]any{ "bio": "Developer", "website": "john.dev", }, "tags": []any{ map[string]any{"name": "go", "color": "blue"}, map[string]any{"name": "web", "color": "green"}, },}
user, err := dd.New[User](data)5.5. Typed Maps - Structured Collections
Section titled “5.5. Typed Maps - Structured Collections”maps with typed keys and values
// define struct with typed mapstype ServerCluster struct { Name string `dd:"name"` Servers map[int]ServerConfig `dd:"servers"` // int keys Cache map[string]CachePolicy `dd:"cache"` // string keys Flags map[bool]string `dd:"flags"` // bool keys}
type ServerConfig struct { Host string `dd:"host"` Port int `dd:"port"`}
type CachePolicy struct { TTL int `dd:"ttl"` Enabled bool `dd:"enabled"`}
// JSON data (map keys are always strings in JSON/YAML)data := map[string]any{ "name": "production", "servers": map[string]any{ "1": map[string]any{"host": "server1.example.com", "port": 8080}, "2": map[string]any{"host": "server2.example.com", "port": 8080}, "10": map[string]any{"host": "server10.example.com", "port": 8081}, }, "cache": map[string]any{ "users": map[string]any{"ttl": 300, "enabled": true}, "sessions": map[string]any{"ttl": 600, "enabled": true}, }, "flags": map[string]any{ "true": "active", "false": "inactive", },}
// bind with automatic key type conversioncluster, err := dd.New[ServerCluster](data)
// access with typed keys (strings "1", "2", "10" → int 1, 2, 10)server1 := cluster.Servers[1] // directly use int keyfmt.Println(server1.Host) // "server1.example.com"
userCache := cluster.Cache["users"] // string keyfmt.Println(userCache.TTL) // 300
active := cluster.Flags[true] // bool keyfmt.Println(active) // "active"key type coercion
since JSON/YAML always use string keys, dd automatically converts them to the target key type:
map[int]T:{"1": ...}→ key becomes1(int)map[int64]T:{"42": ...}→ key becomes42(int64)map[uint]T:{"10": ...}→ key becomes10(uint)map[bool]T:{"true": ...}→ key becomestrue(bool)map[string]T: no conversion needed
nested maps
type NestedConfig struct { Environments map[string]map[string]string `dd:"envs"`}
data := map[string]any{ "envs": map[string]any{ "dev": map[string]any{ "db_host": "localhost", "api_url": "http://localhost:8080", }, "prod": map[string]any{ "db_host": "db.prod.example.com", "api_url": "https://api.example.com", }, },}
config, _ := dd.New[NestedConfig](data)dbHost := config.Environments["prod"]["db_host"] // "db.prod.example.com"maps with pointer values
type UserRegistry struct { Users map[int]*User `dd:"users"`}
type User struct { Name string `dd:"name"` Email string `dd:"email"`}
data := map[string]any{ "users": map[string]any{ "1001": map[string]any{"name": "Alice", "email": "alice@example.com"}, "1002": map[string]any{"name": "Bob", "email": "bob@example.com"}, },}
registry, _ := dd.New[UserRegistry](data)alice := registry.Users[1001] // *Userfmt.Println(alice.Name) // "Alice"maps with slice values
type GroupRegistry struct { Groups map[string][]string `dd:"groups"`}
data := map[string]any{ "groups": map[string]any{ "admins": []any{"alice", "bob"}, "developers": []any{"charlie", "diana", "eve"}, "viewers": []any{"frank"}, },}
registry, _ := dd.New[GroupRegistry](data)admins := registry.Groups["admins"] // []string{"alice", "bob"}unbind with typed maps
when unbinding, all map keys are converted to strings for JSON/YAML compatibility:
type Config struct { Servers map[int]string `dd:"servers"`}
config := Config{ Servers: map[int]string{ 1: "server1.example.com", 2: "server2.example.com", 10: "server10.example.com", },}
data, _ := dd.Unbind(config)// result: {"servers": {"1": "server1.example.com", "2": "server2.example.com", "10": "server10.example.com"}}// int keys 1, 2, 10 → string keys "1", "2", "10"use cases
typed maps are ideal for:
- id-based lookups:
map[int]Userfor user registries - configuration sets:
map[string]ServerConfigfor environment-specific configs - indexed data:
map[uint64]Recordfor database-style access - flag mappings:
map[bool]stringfor conditional values - enum-like keys: when you need type-safe key access
6. Validation - Required Fields and Errors
Section titled “6. Validation - Required Fields and Errors”Field validation and error handling
type User struct { Name string `dd:"+required"` Email string `dd:"+required"` Age int `dd:",+required"`}
// Missing required fielddata := map[string]any{ "name": "John", // email missing "age": 30,}
user, err := dd.New[User](data)// err: "User.Email: required field missing"
// Check for specific error typesif bindErr, ok := err.(*dd.BindError); ok { fmt.Printf("Field: %s, Error: %s\n", bindErr.Field, bindErr.Message)}7. Merge - Configuration Layering
Section titled “7. Merge - Configuration Layering”Overlay data onto existing structs with defaults
// Start with defaultsconfig := &ServerConfig{ Host: "localhost", Port: 8080, Timeout: 30, Debug: false,}
// Overlay user configuration (only overrides specified fields)userConfig := map[string]any{ "host": "api.example.com", "debug": true, // port and timeout preserved from defaults}
err := dd.Merge(config, userConfig)// Result: Host="api.example.com", Port=8080, Timeout=30, Debug=true8. Custom Converters - Specialized Types
Section titled “8. Custom Converters - Specialized Types”Handle custom types with validation
type Email string
type EmailConverter struct{}
func (c *EmailConverter) FromRaw(raw interface{}) (interface{}, error) { s, ok := raw.(string) if !ok { return nil, fmt.Errorf("expected string for email") } if !strings.Contains(s, "@") { return nil, fmt.Errorf("invalid email format") } return Email(s), nil}
func (c *EmailConverter) ToRaw(value interface{}) (interface{}, error) { email, ok := value.(Email) if !ok { return nil, fmt.Errorf("expected Email type") } return string(email), nil}
// Use converteropts := &dd.Options{ Converters: map[reflect.Type]dd.Converter{ reflect.TypeOf(Email("")): &EmailConverter{}, },}
type User struct { Email Email `dd:"email"`}
user, err := dd.New[User](data, opts) // validates email format9. Custom Marshaling - Full Control
Section titled “9. Custom Marshaling - Full Control”Complete control over binding/unbinding
type CustomTime struct { time.Time}
// Control how this type is created from datafunc (c *CustomTime) UnmarshalDf(data any) error { dateStr, ok := data.(string) if !ok { return fmt.Errorf("expected string for CustomTime") } t, err := time.Parse("2006-01-02", dateStr) if err != nil { return err } c.Time = t return nil}
// Control how this type becomes datafunc (c CustomTime) MarshalDf() (any, error) { return c.Time.Format("2006-01-02"), nil}
// dd automatically uses these methods10. Dynamic Types - Runtime Polymorphism
Section titled “10. Dynamic Types - Runtime Polymorphism”Different types based on runtime data
// Types that implement Dynamic interfacetype EmailAction struct { Recipient string `dd:"recipient"` Subject string `dd:"subject"`}
func (e EmailAction) Type() string { return "email" }func (e EmailAction) ToMap() (map[string]any, error) { return map[string]any{ "recipient": e.Recipient, "subject": e.Subject, }, nil}
type SlackAction struct { Channel string `dd:"channel"` Message string `dd:"message"`}
func (s SlackAction) Type() string { return "slack" }func (s SlackAction) ToMap() (map[string]any, error) { return map[string]any{ "channel": s.Channel, "message": s.Message, }, nil}
// Use in polymorphic fieldstype Notification struct { Name string `dd:"name"` Action dd.Dynamic `dd:"action"`}
// Configure type discriminationopts := &dd.Options{ DynamicBinders: map[string]func(map[string]any) (dd.Dynamic, error){ "email": func(m map[string]any) (dd.Dynamic, error) { action, err := dd.New[EmailAction](m) return *action, err }, "slack": func(m map[string]any) (dd.Dynamic, error) { action, err := dd.New[SlackAction](m) return *action, err }, },}
// Data with type discriminatordata := map[string]any{ "name": "Welcome", "action": map[string]any{ "type": "email", // discriminator "recipient": "user@example.com", "subject": "Welcome!", },}
notification, err := dd.New[Notification](data, opts)11. Object References - Linked Data
Section titled “11. Object References - Linked Data”Handle object references with cycle detection
type User struct { ID string `dd:"id"` Name string `dd:"name"`}
func (u *User) GetId() string { return u.ID }
type Document struct { ID string `dd:"id"` Title string `dd:"title"` Author *dd.Pointer[*User] `dd:"author"`}
func (d *Document) GetId() string { return d.ID }
// Data with $ref referencesdata := map[string]any{ "users": []any{ map[string]any{"id": "user1", "name": "Alice"}, }, "documents": []any{ map[string]any{ "id": "doc1", "title": "Guide", "author": map[string]any{"$ref": "user1"}, }, },}
// Two-phase processvar container DataContainerdd.Bind(&container, data) // Phase 1: bind with $ref stringsdd.Link(&container) // Phase 2: resolve references
// Access resolved objectsauthor := container.Documents[0].Author.Resolve()12. Advanced Linking - Performance and Control
Section titled “12. Advanced Linking - Performance and Control”Advanced reference resolution with caching
// Create linker with optionslinker := dd.NewLinker(dd.LinkerOptions{ EnableCaching: true, // cache object registries AllowPartialResolution: false, // fail if any refs unresolved})
// Multi-stage linking for complex scenarioslinker.Register(&users) // register objects from multiple sourceslinker.Register(&documents)linker.Register(&projects)
err := linker.ResolveReferences() // resolve all at once
// OR use convenience methoderr := linker.Link(&container) // register + resolve in one callCore Functions
Section titled “Core Functions”| Function | Purpose | Use Case |
|---|---|---|
dd.New[T](data) | Create struct from map | Type-safe allocation |
dd.Bind(&struct, data) | Populate existing struct | Manual allocation control |
dd.Unbind(struct) | Convert struct to map | Serialization, APIs |
dd.Merge(&struct, data) | Overlay data on defaults | Configuration systems |
dd.BindFromJSON[T](file) | Load from JSON file | Configuration loading |
dd.UnbindToYAML(struct, file) | Save to YAML file | Configuration persistence |
dd.Link(&container) | Resolve object references | Complex data relationships |
Common Patterns
Section titled “Common Patterns”Configuration Loading
Section titled “Configuration Loading”// Multi-layer configurationconfig := getDefaultConfig()dd.MergeFromYAML(config, "app.yaml") // base configdd.MergeFromYAML(config, "app.prod.yaml") // environmentdd.Merge(config, getEnvOverrides()) // environment varsAPI Integration
Section titled “API Integration”// HTTP API → structresp, _ := http.Get("https://api.example.com/user")var data map[string]anyjson.NewDecoder(resp.Body).Decode(&data)user, _ := dd.New[User](data)
// struct → HTTP APIuserData, _ := dd.Unbind(user)json.NewEncoder(w).Encode(userData)Database Integration
Section titled “Database Integration”// Database row → structrow := db.QueryRow("SELECT data FROM users WHERE id = ?", id)var jsonData stringrow.Scan(&jsonData)var data map[string]anyjson.Unmarshal([]byte(jsonData), &data)user, _ := dd.New[User](data)See dd/examples/ for complete working examples of each feature.