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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.