mapstructure

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jan 16, 2026 License: MIT Imports: 8 Imported by: 0

README

mapstructure

Go Reference Go Report Card CI codecov

Go library for decoding map[string]any values into strongly-typed structs with automatic type conversion and comprehensive struct tag support.

Features

  • Automatic type conversion - string ↔ int, bool, float, and more
  • Struct tag support - Flexible field name mapping with schema, json, or custom tags
  • Nested structs - Deep nesting and embedded struct handling
  • Default values - Set defaults via default tag
  • Custom converters - Register converters for custom types
  • Thread-safe - Concurrent unmarshaling with cached metadata
  • Zero allocations - Efficient slice and struct unmarshaling
  • Battle-tested - 90%+ test coverage with comprehensive edge cases
  • Minimal dependencies - Only 1 runtime dependency (tagparser)

Installation

go get github.com/talav/mapstructure

Quick Start

package main

import (
    "fmt"
    "github.com/talav/mapstructure"
)

type Person struct {
    Name string `schema:"name"`
    Age  int    `schema:"age"`
}

func main() {
    data := map[string]any{
        "name": "Alice",
        "age":  "30", // string automatically converted to int
    }

    var person Person
    if err := mapstructure.Unmarshal(data, &person); err != nil {
        panic(err)
    }

    fmt.Printf("%+v\n", person) // {Name:Alice Age:30}
}

Usage

Basic Unmarshaling

The simplest way to use mapstructure is with the convenience function:

type Config struct {
    Host string `schema:"host"`
    Port int    `schema:"port"`
}

data := map[string]any{
    "host": "localhost",
    "port": 8080,
}

var config Config
err := mapstructure.Unmarshal(data, &config)
Struct Tags

By default, the schema tag is used for field mapping:

type Config struct {
    ServerHost string `schema:"server_host"`
    ServerPort int    `schema:"port"`
    Debug      bool   `schema:"debug"`
    Ignored    string `schema:"-"` // Skip this field
}
Tag Behavior
schema:"name" Use "name" as the map key
schema:"-" Skip field entirely
No tag Use Go field name
Type Conversion

Built-in converters handle common type conversions automatically:

Target Type Accepted Input Types Example
string string, bool, int, uint, float, []byte 42"42"
bool bool, int, uint, float, string "true", 1true
int, int8...int64 int, uint, float, bool, string "42"42
uint, uint8...uint64 int, uint, float, bool, string "42"uint(42)
float32, float64 int, uint, float, bool, string "3.14"3.14
[]byte []byte, string, []any, io.Reader "Hello"[]byte("Hello")
io.ReadCloser io.ReadCloser, io.Reader, []byte, string Wraps in io.NopCloser

Type conversion examples:

type Example struct {
    Count   int     `schema:"count"`
    Price   float64 `schema:"price"`
    Enabled bool    `schema:"enabled"`
}

// All of these work:
data := map[string]any{
    "count":   "42",    // string → int
    "price":   100,     // int → float64
    "enabled": 1,       // int → bool
}

var ex Example
mapstructure.Unmarshal(data, &ex)
// ex.Count = 42, ex.Price = 100.0, ex.Enabled = true
Default Values

Use the default tag to set default values for missing fields:

type Config struct {
    Host    string `schema:"host" default:"localhost"`
    Port    int    `schema:"port" default:"8080"`
    Debug   bool   `schema:"debug" default:"false"`
    Timeout int    `schema:"timeout" default:"30"`
}

data := map[string]any{
    "host": "example.com",
    // port, debug, timeout are missing
}

var config Config
mapstructure.Unmarshal(data, &config)
// config.Host = "example.com"
// config.Port = 8080 (from default)
// config.Debug = false (from default)
// config.Timeout = 30 (from default)
Nested Structs

Nested structs are handled automatically:

type Address struct {
    City    string `schema:"city"`
    Country string `schema:"country"`
}

type Person struct {
    Name    string  `schema:"name"`
    Address Address `schema:"address"`
}

data := map[string]any{
    "name": "Alice",
    "address": map[string]any{
        "city":    "New York",
        "country": "USA",
    },
}

var person Person
mapstructure.Unmarshal(data, &person)
Embedded Structs

Embedded structs support both promoted and named field access:

type Timestamps struct {
    CreatedAt string `schema:"created_at"`
    UpdatedAt string `schema:"updated_at"`
}

type User struct {
    Timestamps        // Embedded - fields promoted to parent
    Name       string `schema:"name"`
}

// Option 1: Promoted fields (flat structure)
data1 := map[string]any{
    "name":       "Alice",
    "created_at": "2024-01-01",
    "updated_at": "2024-01-02",
}

// Option 2: Named embedded access (nested)
data2 := map[string]any{
    "name": "Alice",
    "Timestamps": map[string]any{
        "created_at": "2024-01-01",
        "updated_at": "2024-01-02",
    },
}

// Both work!
var user User
mapstructure.Unmarshal(data1, &user) // Promoted fields
mapstructure.Unmarshal(data2, &user) // Named access
Pointers and Slices
type Config struct {
    Tags    []string `schema:"tags"`
    Count   *int     `schema:"count"`
    Data    []byte   `schema:"data"`
}

data := map[string]any{
    "tags":  []any{"go", "api"},
    "count": 42,
    "data":  []any{72, 101, 108, 108, 111}, // Converts to []byte("Hello")
}

var config Config
mapstructure.Unmarshal(data, &config)

⚠️ Detecting missing fields:

By default, missing fields get zero values. Use pointers to distinguish missing from zero:

type Config struct {
    Port    *int  `schema:"port"`    // nil if missing
    Enabled *bool `schema:"enabled"` // nil if missing
}

data := map[string]any{
    "port": 8080,
    // enabled is missing
}

var config Config
mapstructure.Unmarshal(data, &config)

if config.Port != nil {
    fmt.Println(*config.Port) // 8080
}

if config.Enabled == nil {
    fmt.Println("enabled not provided") // This prints
}
Custom Tag Names

Use a different tag (e.g., json, yaml, db):

cache := mapstructure.NewStructMetadataCache("json", "default")
converters := mapstructure.NewDefaultConverterRegistry()
unmarshaler := mapstructure.NewUnmarshaler(cache, converters)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

data := map[string]any{"name": "Alice", "email": "[email protected]"}
var user User
unmarshaler.Unmarshal(data, &user)

Use custom default value tags:

// Use "dflt" tag for default values instead of "default"
cache := mapstructure.NewStructMetadataCache("schema", "dflt")
unmarshaler := mapstructure.NewUnmarshaler(cache, mapstructure.NewDefaultConverterRegistry())

type Config struct {
    Host string `schema:"host" dflt:"localhost"`
    Port int    `schema:"port" dflt:"8080"`
}

// Missing values will use defaults from "dflt" tag
data := map[string]any{}
var config Config
unmarshaler.Unmarshal(data, &config)
// Result: Config{Host: "localhost", Port: 8080}

Cleaner API using defaults:

// Uses "schema" + "default" tags
cache := mapstructure.NewDefaultStructMetadataCache()
unmarshaler := mapstructure.NewUnmarshaler(cache, mapstructure.NewDefaultConverterRegistry())

// Or even simpler:
unmarshaler := mapstructure.NewDefaultUnmarshaler()
Custom Converters

Register converters for custom types:

import (
    "reflect"
    "time"
)

// Define a converter for time.Time
timeConverter := func(value any) (reflect.Value, error) {
    s, ok := value.(string)
    if !ok {
        return reflect.Value{}, fmt.Errorf("expected string for time.Time")
    }
    t, err := time.Parse(time.RFC3339, s)
    if err != nil {
        return reflect.Value{}, err
    }
    return reflect.ValueOf(t), nil
}

// Register the converter
converters := mapstructure.NewDefaultConverterRegistry(map[reflect.Type]mapstructure.Converter{
    reflect.TypeOf(time.Time{}): timeConverter,
})

cache := mapstructure.NewStructMetadataCache("schema", "default")
unmarshaler := mapstructure.NewUnmarshaler(cache, converters)

type Event struct {
    Name      string    `schema:"name"`
    Timestamp time.Time `schema:"timestamp"`
}

data := map[string]any{
    "name":      "meeting",
    "timestamp": "2024-01-15T10:30:00Z",
}

var event Event
unmarshaler.Unmarshal(data, &event)

Custom converter for enums:

type Status int

const (
    StatusPending Status = iota
    StatusActive
    StatusClosed
)

statusConverter := func(value any) (reflect.Value, error) {
    s, ok := value.(string)
    if !ok {
        return reflect.Value{}, fmt.Errorf("expected string")
    }
    
    switch s {
    case "pending":
        return reflect.ValueOf(StatusPending), nil
    case "active":
        return reflect.ValueOf(StatusActive), nil
    case "closed":
        return reflect.ValueOf(StatusClosed), nil
    default:
        return reflect.Value{}, fmt.Errorf("unknown status: %s", s)
    }
}

Real-World Examples

API Response Parsing
type APIResponse struct {
    Status  string          `schema:"status"`
    Code    int             `schema:"code"`
    Message string          `schema:"message"`
    Data    json.RawMessage `schema:"data"`
}

type UserData struct {
    ID    int    `schema:"id"`
    Name  string `schema:"name"`
    Email string `schema:"email"`
}

// Parse outer response
var response APIResponse
mapstructure.Unmarshal(apiData, &response)

// Parse nested data if needed
if response.Status == "success" {
    var userData map[string]any
    json.Unmarshal(response.Data, &userData)
    
    var user UserData
    mapstructure.Unmarshal(userData, &user)
}

Error Handling

The library provides structured error types for better error handling:

Structured Error Types

ConversionError - Type conversion failures:

type Config struct {
    Port int `schema:"port"`
}

data := map[string]any{
    "port": "not-a-number",
}

var config Config
err := mapstructure.Unmarshal(data, &config)
if err != nil {
    // Check if it's a conversion error
    var convErr *mapstructure.ConversionError
    if errors.As(err, &convErr) {
        fmt.Printf("Field: %s\n", convErr.FieldPath)      // "port"
        fmt.Printf("Value: %v\n", convErr.Value)          // "not-a-number"
        fmt.Printf("Target: %v\n", convErr.TargetType)    // int
        fmt.Printf("Cause: %v\n", convErr.Cause)          // parsing error
    }
}

ValidationError - Input validation failures:

// Non-pointer error
err := mapstructure.Unmarshal(data, Config{}) // Wrong!
var valErr *mapstructure.ValidationError
if errors.As(err, &valErr) {
    fmt.Println(valErr.Message) // "result must be a pointer"
}

// Nil pointer error
var config *Config
err := mapstructure.Unmarshal(data, config)
// ValidationError: "result pointer is nil"

Error messages include field paths:

type Nested struct {
    Inner struct {
        Value int `schema:"value"`
    } `schema:"inner"`
}

data := map[string]any{
    "inner": map[string]any{
        "value": "invalid",
    },
}

var nested Nested
err := mapstructure.Unmarshal(data, &nested)
// Error: inner.value: cannot convert string to int

Performance

The library is optimized for production use:

Key optimizations:

  • Struct metadata caching - Reflection done once per type
  • Fast-path slice operations - Zero-copy for compatible types
  • Immutable converter registry - Lock-free concurrent reads

Thread Safety

Safe for concurrent use:

  • StructMetadataCache uses sync.Map for concurrent access
  • ConverterRegistry is immutable after construction
  • Unmarshaler is safe for concurrent unmarshaling
// Safe: Shared unmarshaler across goroutines
var unmarshaler = mapstructure.NewDefaultUnmarshaler()

func handler1() {
    var result1 Type1
    unmarshaler.Unmarshal(data1, &result1) // Concurrent safe
}

func handler2() {
    var result2 Type2
    unmarshaler.Unmarshal(data2, &result2) // Concurrent safe
}

Testing

# Run tests
go test -v

# Run with race detector
go test -race

# Run with coverage
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

API Reference

Top-Level Functions
// Unmarshal transforms map[string]any into a Go struct
// Uses default settings (schema tag, standard converters)
func Unmarshal(data map[string]any, result any) error
Types
// Unmarshaler handles unmarshaling with custom configuration
type Unmarshaler struct { /* ... */ }

// Converter converts a value to a reflect.Value
type Converter func(value any) (reflect.Value, error)

// ConverterRegistry manages type converters
type ConverterRegistry struct { /* ... */ }

// StructMetadataCache caches struct field metadata
type StructMetadataCache struct { /* ... */ }

// FieldMetadata holds cached struct field information
type FieldMetadata struct {
    StructFieldName string
    MapKey          string
    Index           int
    Type            reflect.Type
    Embedded        bool
    Default         *string
}
Constructors
// NewUnmarshaler creates a new unmarshaler with explicit dependencies
func NewUnmarshaler(cache *StructMetadataCache, converters *ConverterRegistry) *Unmarshaler

// NewDefaultUnmarshaler creates an unmarshaler with default settings
func NewDefaultUnmarshaler() *Unmarshaler

// NewStructMetadataCache creates a metadata cache
// tagName specifies which tag to read for field mapping (e.g., "schema", "json", "yaml")
// defaultTagName specifies which tag to read for default values (e.g., "default")
// Use "-" for tagName to ignore tags and map by field names only
// Empty strings default to "schema" and "default" respectively
func NewStructMetadataCache(tagName, defaultTagName string) *StructMetadataCache

// NewDefaultStructMetadataCache creates a cache with default tag names ("schema", "default")
func NewDefaultStructMetadataCache() *StructMetadataCache

// NewDefaultConverterRegistry creates a registry with standard converters
// additional converter maps can override or extend defaults
func NewDefaultConverterRegistry(additional ...map[reflect.Type]Converter) *ConverterRegistry

// NewConverterRegistry creates a registry with only specified converters
func NewConverterRegistry(converters map[reflect.Type]Converter) *ConverterRegistry
Methods
// Unmarshal transforms map[string]any into result struct
func (u *Unmarshaler) Unmarshal(data map[string]any, result any) error

// GetMetadata retrieves or builds cached struct field metadata
// Safe for concurrent use; useful for pre-warming cache or introspection
func (c *StructMetadataCache) GetMetadata(typ reflect.Type) *StructMetadata

// Find looks up a converter for the given type
func (r *ConverterRegistry) Find(typ reflect.Type) (Converter, bool)

Limitations and Best Practices

Missing Fields vs Zero Values

⚠️ Important: By default, missing fields receive Go zero values:

data := map[string]any{"name": "Alice"}
var person Person
mapstructure.Unmarshal(data, &person)
// person.Age = 0 (zero value, not explicitly set!)

Solutions:

  1. Use pointers to detect missing fields:

    type Person struct {
        Age *int `schema:"age"` // nil if missing
    }
    
  2. Use default tags for explicit defaults:

    type Person struct {
        Age int `schema:"age" default:"18"`
    }
    
  3. Validate after unmarshaling if fields are required

Type Safety

The library performs best-effort type conversion:

// These all work (maybe not what you want):
data := map[string]any{
    "age": "abc", // Will fail with error ✓
    "age": 3.14,  // Converts to 3 (truncates)
    "age": true,  // Converts to 1
}

API Stability

This library follows semantic versioning. The public API is stable for v1.x:

Stable APIs:

  • Unmarshal()
  • NewUnmarshaler(), NewDefaultUnmarshaler()
  • NewStructMetadataCache(), NewDefaultStructMetadataCache()
  • NewDefaultConverterRegistry(), NewConverterRegistry()
  • All exported types and methods
Development Commands
# Run tests
go test -v ./...

# Run with race detector
go test -race ./...

# Run linters
golangci-lint run

# Run benchmarks
go test -bench=. -benchmem

# Generate coverage report
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

License

MIT License - see LICENSE file for details.

Credits

Developed by Talav.

Tag parsing powered by tagparser.


Questions? Open an issue or discussion on GitHub.

Found a bug? Please report it with a minimal reproduction case.

Documentation

Index

Constants

View Source
const (
	// DefaultTagName is the default struct tag name for field mapping.
	DefaultTagName = "schema"

	// DefaultValueTagName is the default struct tag name for default values.
	DefaultValueTagName = "default"
)

Variables

This section is empty.

Functions

func Unmarshal

func Unmarshal(data map[string]any, result any) error

Unmarshal transforms map[string]any into a Go struct pointed to by result. result must be a pointer to the target type. This is a convenience function that uses a shared default unmarshaler.

Types

type ConversionError

type ConversionError struct {
	FieldPath  string
	Value      any
	TargetType reflect.Type
	Cause      error
}

ConversionError represents a type conversion failure.

func NewConversionError

func NewConversionError(fieldPath string, value any, targetType reflect.Type, cause error) *ConversionError

NewConversionError creates a new ConversionError.

func (*ConversionError) Error

func (e *ConversionError) Error() string

func (*ConversionError) Unwrap

func (e *ConversionError) Unwrap() error

type Converter

type Converter func(value any) (reflect.Value, error)

Converter converts a value to a reflect.Value of a specific type.

type ConverterRegistry

type ConverterRegistry struct {
	// contains filtered or unexported fields
}

ConverterRegistry manages type converters. Immutable after construction, safe for concurrent reads.

func NewConverterRegistry

func NewConverterRegistry(converters map[reflect.Type]Converter) *ConverterRegistry

NewConverterRegistry creates a registry with the given converters. If converters is nil, an empty registry is created.

func NewDefaultConverterRegistry

func NewDefaultConverterRegistry(additional ...map[reflect.Type]Converter) *ConverterRegistry

NewDefaultConverterRegistry creates a registry with standard type converters. Additional converter maps can be provided to extend or override defaults. If multiple maps are provided, they are merged in order (later maps override earlier ones).

func (*ConverterRegistry) Find

func (r *ConverterRegistry) Find(typ reflect.Type) (Converter, bool)

Find finds a converter for the given type. Lock-free read, safe for concurrent use.

type FieldMetadata

type FieldMetadata struct {
	StructFieldName string       // Go field name
	MapKey          string       // Key to lookup in map
	Index           int          // Field index for reflection
	Type            reflect.Type // Field type
	Embedded        bool         // Anonymous/embedded struct
	Default         *string      // Raw default value from `default` tag, nil if no tag
}

FieldMetadata holds cached struct field information.

type StructMetadata

type StructMetadata struct {
	Fields []FieldMetadata
}

StructMetadata holds cached metadata for a struct type.

type StructMetadataCache

type StructMetadataCache struct {
	// contains filtered or unexported fields
}

StructMetadataCache provides caching for struct field metadata.

func NewDefaultStructMetadataCache

func NewDefaultStructMetadataCache() *StructMetadataCache

NewDefaultStructMetadataCache creates a struct metadata cache with default tag names. Uses "schema" for field mapping and "default" for default values. This is equivalent to NewStructMetadataCache(DefaultTagName, DefaultValueTagName).

func NewStructMetadataCache

func NewStructMetadataCache(tagName, defaultTagName string) *StructMetadataCache

NewStructMetadataCache creates a new struct metadata cache. tagName specifies which tag to read for field mapping (e.g., "schema", "json", "yaml"). defaultTagName specifies which tag to read for default values (e.g., "default"). Use "-" for tagName to ignore all tags and map fields by their Go struct field names. Empty strings default to "schema" and "default" respectively.

func (*StructMetadataCache) GetMetadata

func (c *StructMetadataCache) GetMetadata(typ reflect.Type) *StructMetadata

GetMetadata retrieves or builds cached struct field metadata for the given type. This method is safe for concurrent use and will cache the result for subsequent calls.

This is useful for:

  • Pre-warming the cache before hot paths
  • Introspecting struct metadata for tooling
  • Testing cache behavior

type Unmarshaler

type Unmarshaler struct {
	// contains filtered or unexported fields
}

Unmarshaler handles unmarshaling of maps to Go structs.

func NewDefaultUnmarshaler

func NewDefaultUnmarshaler() *Unmarshaler

NewDefaultUnmarshaler creates a new unmarshaler with default settings. Uses "schema" tags for field mapping and "default" tags for default values.

func NewUnmarshaler

func NewUnmarshaler(fieldCache *StructMetadataCache, converters *ConverterRegistry) *Unmarshaler

NewUnmarshaler creates a new unmarshaler with explicit dependencies. For custom configurations, construct the cache and converters separately:

// With custom tag
cache := NewStructMetadataCache("json", "default")
converters := NewDefaultConverterRegistry()
u := NewUnmarshaler(cache, converters)

// With custom converters
cache := NewDefaultStructMetadataCache()
converters := NewDefaultConverterRegistry(customConverters)
u := NewUnmarshaler(cache, converters)

func (*Unmarshaler) Unmarshal

func (u *Unmarshaler) Unmarshal(data map[string]any, result any) error

Unmarshal transforms map[string]any into a Go struct pointed to by result. result must be a pointer to the target type.

type ValidationError

type ValidationError struct {
	Message string
}

ValidationError represents a validation failure for the result pointer.

func NewValidationError

func NewValidationError(message string) *ValidationError

NewValidationError creates a new ValidationError.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Jump to

Keyboard shortcuts

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