mapper

package module
v1.0.4 Latest Latest
Warning

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

Go to latest
Published: Jan 18, 2026 License: MIT Imports: 4 Imported by: 0

README

mapper

Go Reference Go Report Card CI

A zero-boilerplate, reflection-based struct mapper for Go. Automatically maps fields between structs with tags, nested paths, patch semantics, and slice support.

Features

  • Zero Boilerplate - No code generation, no manual field assignments
  • Tag-Based Aliasing - Map fields with different names using struct tags
  • String Conversion - Automatic string-to-primitive conversion via mapconv tag
  • Nested Structs - Recursive mapping of arbitrarily nested structures
  • Deep Copying - Slices and maps are deep-copied, not shared
  • Pointer Flexibility - Seamless conversion between pointer and value types
  • Patch Semantics - Skip zero values for partial updates
  • Strict Mode - Ensure all destination fields are populated
  • Thread Safe - Safe for concurrent use with internal caching
  • Performance Optimized - Struct metadata caching minimizes reflection overhead

Installation

go get github.com/tariklabs/mapper

Requires Go 1.21 or later.

Quick Start

package main

import (
    "fmt"
    "log"

    "github.com/tariklabs/mapper"
)

type UserDTO struct {
    FullName string `map:"Name"`
    Email    string
    Age      string `mapconv:"int"`
}

type User struct {
    Name  string
    Email string
    Age   int
}

func main() {
    dto := UserDTO{
        FullName: "Rafa Smith",
        Email:    "rafa@example.com",
        Age:      "30",
    }

    var user User
    if err := mapper.Map(&user, dto); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%+v\n", user)
    // Output: {Name:Rafa Smith Email:rafa@example.com Age:30}
}

Documentation

Full API documentation is available on pkg.go.dev.

Usage

Basic Mapping

Fields are matched by name (case-sensitive):

type Source struct {
    Name  string
    Email string
    Age   int
}

type Destination struct {
    Name  string
    Email string
    Age   int
}

src := Source{Name: "Rafa", Email: "rafa@example.com", Age: 30}
var dst Destination

err := mapper.Map(&dst, src)
// dst = {Name: "Rafa", Email: "rafa@example.com", Age: 30}
Tag-Based Field Aliasing

Use the map tag to map fields with different names:

type APIResponse struct {
    UserName    string `map:"Name"`
    UserEmail   string `map:"Email"`
    YearsOld    int    `map:"Age"`
}

type User struct {
    Name  string
    Email string
    Age   int
}

response := APIResponse{
    UserName:  "Bruno",
    UserEmail: "bruno@example.com",
    YearsOld:  25,
}
var user User

err := mapper.Map(&user, response)
// user = {Name: "Bruno", Email: "bruno@example.com", Age: 25}
String-to-Type Conversion

Use the mapconv tag to convert string fields to typed values:

type FormInput struct {
    Age      string `mapconv:"int"`
    Score    string `mapconv:"float64"`
    Active   string `mapconv:"bool"`
    Quantity string `mapconv:"uint"`
}

type Product struct {
    Age      int
    Score    float64
    Active   bool
    Quantity uint
}

input := FormInput{
    Age:      "42",
    Score:    "95.5",
    Active:   "true",
    Quantity: "100",
}
var product Product

err := mapper.Map(&product, input)
// product = {Age: 42, Score: 95.5, Active: true, Quantity: 100}

Supported conversion types: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool

Tags can be combined:

type Input struct {
    UserAge string `map:"Age" mapconv:"int"`
}
Nested Structs

Nested structs are mapped recursively:

type SrcAddress struct {
    Street string
    City   string
}

type SrcPerson struct {
    Name    string
    Address SrcAddress
}

type DstAddress struct {
    Street string
    City   string
}

type DstPerson struct {
    Name    string
    Address DstAddress
}

src := SrcPerson{
    Name: "Rafa",
    Address: SrcAddress{
        Street: "123 Main St",
        City:   "Seattle",
    },
}
var dst DstPerson

err := mapper.Map(&dst, src)
// dst.Address = {Street: "123 Main St", City: "Seattle"}
Slices and Maps

Slices and maps are deep-copied with element type conversion:

type Source struct {
    Tags    []string
    Counts  []int32
    Config  map[string]string
    Values  map[string]int32
}

type Destination struct {
    Tags    []string
    Counts  []int64             // Different element type
    Config  map[string]string
    Values  map[string]int64    // Different value type
}

src := Source{
    Tags:   []string{"go", "mapper"},
    Counts: []int32{1, 2, 3},
    Config: map[string]string{"env": "prod"},
    Values: map[string]int32{"a": 100},
}
var dst Destination

err := mapper.Map(&dst, src)
// Deep copy with automatic type conversion

Important: Modifying the source after mapping does not affect the destination.

Pointer Handling

Seamless conversion between pointer and value types:

// Value to pointer
type Source struct {
    Name string
}

type Destination struct {
    Name *string
}

src := Source{Name: "Rafa"}
var dst Destination

err := mapper.Map(&dst, src)
// *dst.Name == "Rafa"
// Pointer to value
type Source struct {
    Name *string
}

type Destination struct {
    Name string
}

name := "Bruno"
src := Source{Name: &name}
var dst Destination

err := mapper.Map(&dst, src)
// dst.Name == "Bruno"

Nil pointers are handled gracefully and do not overwrite destination values.

Options

Use MapWithOptions for customized behavior:

WithTagName

Use a different struct tag for field aliasing:

type Response struct {
    UserName string `json:"name"`
}

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

err := mapper.MapWithOptions(&user, response, mapper.WithTagName("json"))
WithIgnoreZeroSource

Skip zero-value fields for patch/partial update operations:

type User struct {
    Name  string
    Email string
    Age   int
}

existing := User{Name: "Rafa", Email: "rafa@old.com", Age: 25}
patch := User{Name: "Alicia", Email: "", Age: 0}  // Only update Name

err := mapper.MapWithOptions(&existing, patch, mapper.WithIgnoreZeroSource())
// existing = {Name: "Alicia", Email: "rafa@old.com", Age: 25}
WithStrictMode

Return an error if any destination field has no matching source:

type Source struct {
    Name string
}

type Destination struct {
    Name  string
    Email string  // No source field!
}

err := mapper.MapWithOptions(&dst, src, mapper.WithStrictMode())
// Error: no matching source field found for "Email"
WithMaxDepth

Set maximum nesting depth (default: 64):

err := mapper.MapWithOptions(&dst, src, mapper.WithMaxDepth(100))
Combining Options
err := mapper.MapWithOptions(&dst, src,
    mapper.WithTagName("json"),
    mapper.WithIgnoreZeroSource(),
    mapper.WithStrictMode(),
    mapper.WithMaxDepth(100),
)

Error Handling

Errors are returned as *MappingError with detailed context:

err := mapper.Map(&dst, src)
if err != nil {
    var mappingErr *mapper.MappingError
    if errors.As(err, &mappingErr) {
        fmt.Printf("Source type: %s\n", mappingErr.SrcType)
        fmt.Printf("Destination type: %s\n", mappingErr.DstType)
        fmt.Printf("Field path: %s\n", mappingErr.FieldPath)
        fmt.Printf("Reason: %s\n", mappingErr.Reason)
    }
}

Field paths use dot notation for nested fields (Address.City) and bracket notation for slices (Items[0]) and maps (Config[key]).

Common Errors
Reason Cause
dst must be a non-nil pointer to struct Destination is not a valid pointer
src must be a struct or pointer to struct Source is not a struct type
no matching source field found Strict mode: destination field has no source
incompatible field types: X -> Y Types cannot be converted
maximum nesting depth exceeded Depth limit reached (circular reference protection)
cannot convert "X" to Y String conversion failed

Performance

The mapper uses struct metadata caching to minimize reflection overhead. First-time mapping of a struct type incurs reflection cost, but subsequent mappings use cached metadata.

BenchmarkMap_Flat-12                 847845    1412 ns/op    312 B/op    11 allocs/op
BenchmarkMap_Nested-12               398174    2987 ns/op    720 B/op    24 allocs/op
BenchmarkMap_Slice100-12              27862   42891 ns/op  14832 B/op   409 allocs/op

For performance-critical hot paths where nanoseconds matter, consider manual field assignment. For typical application code (API handlers, DTOs, data transformation), the mapper provides an excellent balance of convenience and performance.

See BENCHMARKS.md for detailed profiling instructions.

Thread Safety

All functions are safe for concurrent use. The internal metadata cache uses sync.Map for thread-safe access.

Limitations

  • Exported fields only - Unexported (private) fields cannot be mapped
  • Structs only - Interface types are not supported as field types
  • Built-in conversions - No custom converter functions
  • Depth-based protection - Circular references are protected by depth limit, not runtime detection

Real-World Examples

API Request to Domain Model
type CreateOrderRequest struct {
    CustomerName string            `map:"Customer"`
    TotalPrice   string            `map:"Total" mapconv:"float64"`
    IsPriority   string            `mapconv:"bool"`
    Tags         []string          `map:"Labels"`
    Metadata     map[string]string `map:"Info"`
}

type Order struct {
    Customer   string
    Total      float64
    IsPriority bool
    Labels     []string
    Info       map[string]string
}

func CreateOrder(req CreateOrderRequest) (*Order, error) {
    var order Order
    if err := mapper.Map(&order, req); err != nil {
        return nil, fmt.Errorf("invalid request: %w", err)
    }
    return &order, nil
}
Database Entity to API Response
type UserEntity struct {
    ID        int64
    Username  string
    Email     string
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time
}

type UserResponse struct {
    ID        int64     `json:"id"`
    Username  string    `json:"username"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

func ToResponse(entity UserEntity) UserResponse {
    var response UserResponse
    mapper.Map(&response, entity)  // Only copies matching fields
    return response
}
Partial Update (Patch)
type UpdateUserRequest struct {
    Name  string `json:"name,omitempty"`
    Email string `json:"email,omitempty"`
    Age   int    `json:"age,omitempty"`
}

func UpdateUser(userID int64, req UpdateUserRequest) error {
    user, err := db.GetUser(userID)
    if err != nil {
        return err
    }

    // Only update fields that were provided (non-zero)
    if err := mapper.MapWithOptions(user, req, mapper.WithIgnoreZeroSource()); err != nil {
        return err
    }

    return db.SaveUser(user)
}

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Run tests (go test -v -race ./...)
  4. Run linter (golangci-lint run)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package mapper provides a zero-boilerplate, reflection-based struct mapper for Go. It automatically maps fields between structs using name matching, tag aliasing, and type conversion, with support for nested structs, slices, maps, and pointers.

Quick Start

Basic mapping between structs with matching field names:

type Source struct {
    Name  string
    Email string
    Age   int
}

type Destination struct {
    Name  string
    Email string
    Age   int
}

src := Source{Name: "Rafa", Email: "rafa@example.com", Age: 30}
var dst Destination

err := mapper.Map(&dst, src)
// dst = {Name: "Rafa", Email: "rafa@example.com", Age: 30}

Tag-Based Field Aliasing

Use the "map" tag to map fields with different names:

type APIResponse struct {
    UserName    string `map:"Name"`
    UserEmail   string `map:"Email"`
    YearsOld    int    `map:"Age"`
}

type User struct {
    Name  string
    Email string
    Age   int
}

src := APIResponse{UserName: "Bruno", UserEmail: "bruno@example.com", YearsOld: 25}
var dst User

err := mapper.Map(&dst, src)
// dst = {Name: "Bruno", Email: "bruno@example.com", Age: 25}

String-to-Type Conversion

Use the "mapconv" tag to convert string fields to numeric or boolean types:

type FormInput struct {
    Age      string `mapconv:"int"`
    Score    string `mapconv:"float64"`
    Active   string `mapconv:"bool"`
}

type User struct {
    Age    int
    Score  float64
    Active bool
}

src := FormInput{Age: "42", Score: "95.5", Active: "true"}
var dst User

err := mapper.Map(&dst, src)
// dst = {Age: 42, Score: 95.5, Active: true}

Supported conversion types: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool.

Tags can be combined for aliasing with conversion:

type Input struct {
    UserAge string `map:"Age" mapconv:"int"`
}

Nested Struct Mapping

Nested structs are mapped recursively:

type SrcAddress struct {
    Street string
    City   string
}

type SrcPerson struct {
    Name    string
    Address SrcAddress
}

type DstAddress struct {
    Street string
    City   string
}

type DstPerson struct {
    Name    string
    Address DstAddress
}

src := SrcPerson{
    Name: "Rafa",
    Address: SrcAddress{Street: "123 Main St", City: "Seattle"},
}
var dst DstPerson

err := mapper.Map(&dst, src)
// dst.Address = {Street: "123 Main St", City: "Seattle"}

Slice and Map Deep Copying

Slices and maps are deep-copied to avoid shared references:

type Source struct {
    Tags   []string
    Config map[string]string
}

type Destination struct {
    Tags   []string
    Config map[string]string
}

src := Source{
    Tags:   []string{"go", "mapper"},
    Config: map[string]string{"env": "prod"},
}
var dst Destination

err := mapper.Map(&dst, src)
// Modifying src.Tags after mapping does not affect dst.Tags

Element type conversion is supported for slices and maps:

type Source struct {
    Values []int32
}

type Destination struct {
    Values []int64  // Different element type
}

Pointer Handling

Flexible conversion between pointer and value types:

type Source struct {
    Name string   // value
}

type Destination struct {
    Name *string  // pointer
}

src := Source{Name: "Rafa"}
var dst Destination

err := mapper.Map(&dst, src)
// *dst.Name == "Rafa"

Nil pointers are handled gracefully and do not overwrite destination values.

Options

Use MapWithOptions for customized behavior:

// Use a different tag name
err := mapper.MapWithOptions(&dst, src, mapper.WithTagName("json"))

// Skip zero-value fields (patch semantics)
err := mapper.MapWithOptions(&dst, src, mapper.WithIgnoreZeroSource())

// Error on missing source fields
err := mapper.MapWithOptions(&dst, src, mapper.WithStrictMode())

// Increase max depth for deeply nested structs
err := mapper.MapWithOptions(&dst, src, mapper.WithMaxDepth(100))

// Combine multiple options
err := mapper.MapWithOptions(&dst, src,
    mapper.WithTagName("json"),
    mapper.WithIgnoreZeroSource(),
    mapper.WithStrictMode(),
)

Patch Semantics

Use WithIgnoreZeroSource for partial updates where only non-zero values should be applied:

existing := User{Name: "Rafa", Email: "rafa@old.com", Age: 25}
patch := PatchRequest{Name: "Alicia", Email: "", Age: 0}  // Only update name

err := mapper.MapWithOptions(&existing, patch, mapper.WithIgnoreZeroSource())
// existing = {Name: "Alicia", Email: "rafa@old.com", Age: 25}

Error Handling

Errors are returned as *MappingError with detailed context:

err := mapper.Map(&dst, src)
if err != nil {
    var mappingErr *mapper.MappingError
    if errors.As(err, &mappingErr) {
        fmt.Printf("Error at field %q: %s\n", mappingErr.FieldPath, mappingErr.Reason)
    }
}

Performance

The mapper uses struct metadata caching to minimize reflection overhead. First-time mapping of a struct type incurs reflection cost, but subsequent mappings use cached metadata for faster execution.

For performance-critical code paths where nanoseconds matter, consider manual field assignment. For typical application code (API handlers, DTOs), the mapper provides a good balance of convenience and performance.

Thread Safety

All functions are safe for concurrent use. The internal metadata cache uses sync.Map for thread-safe access.

Limitations

  • Only exported (public) fields are mapped
  • Interface types are not supported as field types
  • No custom converter functions (only built-in type conversions)
  • Circular references are protected by depth limit, not runtime detection

Index

Constants

View Source
const DefaultMaxDepth = 64

DefaultMaxDepth is the default maximum nesting depth for struct mapping. This limit prevents stack overflow from deeply nested or circular references. The default value of 64 is sufficient for most real-world use cases.

Variables

This section is empty.

Functions

func Map

func Map(dst any, src any) error

Map copies fields from src to dst using default configuration.

The dst argument must be a non-nil pointer to a struct. The src argument must be a struct or a non-nil pointer to a struct. Fields are matched by name (case-sensitive) or by the "map" tag value on source fields.

Map performs deep copying for slices, maps, and nested structs. Pointer fields are handled flexibly: values can map to pointers and vice versa.

Only exported fields are mapped. Unexported fields are silently ignored.

Example:

type Source struct {
    Name  string
    Email string `map:"ContactEmail"`
}

type Destination struct {
    Name         string
    ContactEmail string
}

src := Source{Name: "Rafa", Email: "rafa@example.com"}
var dst Destination

if err := mapper.Map(&dst, src); err != nil {
    log.Fatal(err)
}
// dst.Name = "Rafa", dst.ContactEmail = "rafa@example.com"

Returns a *MappingError if mapping fails. Common failure causes include type incompatibility, nil pointers, and string conversion errors.

func MapWithOptions

func MapWithOptions(dst any, src any, opts ...Option) error

MapWithOptions copies fields from src to dst with custom configuration.

Options are applied in order using the functional options pattern. See WithTagName, WithIgnoreZeroSource, WithStrictMode, and WithMaxDepth for available options.

Example with multiple options:

err := mapper.MapWithOptions(&dst, src,
    mapper.WithTagName("json"),       // Use json tags instead of map tags
    mapper.WithIgnoreZeroSource(),    // Skip zero-value fields
    mapper.WithStrictMode(),          // Error on unmapped destination fields
)

Example for patch operations:

// Only update fields that have non-zero values in the patch
existing := User{Name: "Rafa", Age: 30}
patch := UpdateRequest{Name: "Alicia"}  // Age is zero, will be skipped

err := mapper.MapWithOptions(&existing, patch, mapper.WithIgnoreZeroSource())
// existing.Name = "Alicia", existing.Age = 30 (unchanged)

Returns a *MappingError if mapping fails.

Types

type MappingError

type MappingError struct {
	// SrcType is the name of the source struct type (e.g., "main.UserDTO").
	SrcType string

	// DstType is the name of the destination struct type (e.g., "main.User").
	DstType string

	// FieldPath is the path to the field where the error occurred.
	// Uses dot notation for nested fields ("Address.City") and brackets
	// for indices ("Items[0]") and map keys ("Config[key]").
	FieldPath string

	// Reason describes why the mapping failed.
	Reason string
}

MappingError describes a failure that occurred during struct mapping. It provides detailed context about what went wrong and where.

The error message format is:

mapper: cannot map {SrcType} → {DstType} at field "{FieldPath}": {Reason}

FieldPath uses dot notation for nested fields (e.g., "Address.City") and bracket notation for slice indices (e.g., "Items[0].Name") and map keys (e.g., "Config[database].Host").

Common Reasons:

  • "nil src or dst" - nil was passed for source or destination
  • "dst must be a non-nil pointer to struct" - destination is not a valid pointer
  • "src must be a struct or pointer to struct" - source is not a struct type
  • "src is a nil pointer" - source pointer is nil
  • "no matching source field found" - strict mode enabled, field has no match
  • "incompatible field types: X -> Y" - types cannot be converted
  • "maximum nesting depth exceeded" - depth limit reached
  • "cannot convert \"X\" to Y" - string conversion failed
  • "unsupported mapconv target type: X" - invalid mapconv tag value
  • "destination field cannot be set" - field is unexported

Example - Error handling:

err := mapper.Map(&dst, src)
if err != nil {
    var mappingErr *mapper.MappingError
    if errors.As(err, &mappingErr) {
        log.Printf("Mapping failed at %s: %s", mappingErr.FieldPath, mappingErr.Reason)
    }
}

func (*MappingError) Error

func (e *MappingError) Error() string

Error implements the error interface and returns a formatted error message.

type Option

type Option func(*config)

Option configures the behavior of MapWithOptions. Options are applied in the order they are passed.

func WithIgnoreZeroSource

func WithIgnoreZeroSource() Option

WithIgnoreZeroSource configures the mapper to skip assignments when the source field has the zero value for its type. This enables patch semantics where only explicitly set fields are copied.

Zero values that are skipped:

  • Empty string ("")
  • Zero numbers (0, 0.0)
  • false for booleans
  • nil for pointers, slices, and maps
  • Empty structs

Example - Patch operation:

type User struct {
    Name  string
    Email string
    Age   int
}

existing := User{Name: "Rafa", Email: "rafa@old.com", Age: 25}
patch := User{Name: "Alicia", Email: "", Age: 0}  // Only Name should update

err := mapper.MapWithOptions(&existing, patch, mapper.WithIgnoreZeroSource())
// existing = {Name: "Alicia", Email: "rafa@old.com", Age: 25}

Without this option, the empty string and zero would overwrite the existing values.

func WithMaxDepth

func WithMaxDepth(depth int) Option

WithMaxDepth sets the maximum nesting depth for struct mapping. This prevents stack overflow from deeply nested structures or circular references.

The default depth is DefaultMaxDepth (64), which is sufficient for most use cases. Each level of nesting (nested struct, slice element, map value) decrements the depth counter.

Values less than or equal to 0 are ignored, and the default depth is used.

Example:

// For very deeply nested structures
err := mapper.MapWithOptions(&dst, src, mapper.WithMaxDepth(200))

// For strict depth limiting
err := mapper.MapWithOptions(&dst, src, mapper.WithMaxDepth(10))
// Returns error if nesting exceeds 10 levels

When the depth limit is exceeded, the mapper returns a *MappingError with the reason "maximum nesting depth exceeded".

func WithStrictMode

func WithStrictMode() Option

WithStrictMode configures the mapper to return an error when a destination field has no matching source field (by name or tag).

This is useful for ensuring that all destination fields are populated, catching mistakes like typos in field names or missing fields in the source.

Example:

type Source struct {
    Name string
}

type Destination struct {
    Name  string
    Email string  // No matching source field
}

err := mapper.MapWithOptions(&dst, src, mapper.WithStrictMode())
// Returns error: no matching source field found for "Email"

Without strict mode, unmatched destination fields are silently left unchanged.

func WithTagName

func WithTagName(tag string) Option

WithTagName sets the struct tag name used to read field aliases from source struct fields. The default tag name is "map".

This option is useful when you want to reuse existing struct tags (like "json" or "db") for mapping instead of adding separate "map" tags.

Example:

type APIResponse struct {
    UserName string `json:"name"`
    UserAge  int    `json:"age"`
}

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

err := mapper.MapWithOptions(&user, response, mapper.WithTagName("json"))

With this option, the mapper will look for "json" tags instead of "map" tags when determining field aliases.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

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