A compile-time code generator that produces type-safe, zero-dependency struct mapping functions for Go.
go install github.com/hacks1ash/goxmap@latest
//go:generate go run github.com/hacks1ash/goxmap -src User -dst UserDTOgoxmap reads your struct definitions at build time and writes plain Go functions that map one struct to another. No reflection. No runtime cost. No dependencies in the generated code.
Reflection-based mappers like jinzhu/copier or mitchellh/mapstructure resolve field correspondences at runtime. This means a renamed field, a type mismatch, or a missing destination field can slip through code review, pass all tests on the happy path, and panic in production when the edge case finally occurs.
goxmap moves this class of error to compile time. The generator reads the actual type information from go/types, matches fields, and writes explicit assignments. If a type changes after generation, the generated code fails to compile. The build breaks before the code ships.
| goxmap (generated) | Reflection-based | |
|---|---|---|
| Field mismatch detection | Compile error | Silent zero value or runtime panic |
| Type mismatch detection | Build-time error with fix suggestion | Silent conversion or runtime panic |
| Missing field | Build-time warning, skipped | Silent zero value |
| Renamed field | Compile error | Silent zero value |
| Numeric widening | Auto-cast (int64(src.F)) |
Runtime type check + conversion |
| Numeric narrowing | Build-time error (data loss prevention) | Silent truncation |
The feedback loop is seconds (your editor underlines the error) rather than days (a production incident report).
The generated .go files contain only standard field assignments and, where needed, import statements for your own packages. There is no runtime library, no reflect import, and no version coupling between the generated code and the generator tool.
This means:
- Your binary size is unaffected.
- Your dependency tree does not grow.
- The generated code is readable, reviewable, and debuggable.
- You can stop using goxmap at any time; the generated files continue to compile and work.
Generated mappers are 10-150x faster than reflection-based alternatives, allocate 2-100x less memory, and produce significantly fewer heap allocations. See docs/PERFORMANCE.md for full benchmark results.
| Benchmark | Generated (ns/op) | Copier (ns/op) | Mapstructure (ns/op) |
|---|---|---|---|
| Simple (10 flat fields) | 26 | 2,941 | 3,840 |
| Nested (structs + slices) | 48 | 1,878 | 6,550 |
| Complex (deep hierarchy) | 379 | 3,882 | 5,997 |
| Numeric cast (int/float coercion) | 9 | 2,829 | 3,796 |
go install github.com/hacks1ash/goxmap@latestOr use it as a tool dependency (recommended for reproducible builds):
go get -tool github.com/hacks1ash/goxmappackage models
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
type UserDTO struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}//go:generate go run github.com/hacks1ash/goxmap -src User -dst UserDTOgo generate ./...This produces user_d_t_o_mapper_gen.go containing:
// Code generated by goxmap; DO NOT EDIT.
package models
func MapUserToUserDTO(src *User) *UserDTO {
if src == nil { return nil }
dst := &UserDTO{}
dst.ID = src.ID
dst.Name = src.Name
dst.Email = src.Email
dst.Age = src.Age
return dst
}goxmap uses pointer-based function signatures with nil-safe code for all pointer combinations:
func MapUserToUserDTO(src *User) *UserDTO {
if src == nil { return nil }
dst := &UserDTO{}
// Direct assignment for pointer fields
dst.ID = src.ID
// Pointer-to-pointer with nil safety
dst.Email = func() *string {
if src.Email != nil { return src.Email }
return nil
}()
return dst
}Field-level conversions within the struct handle all pointer combinations:
| Source Field | Destination Field | Behavior |
|---|---|---|
T |
T |
Direct assignment |
*T |
*T |
Direct assignment with nil check |
*T |
T |
Nil-checked dereference with zero-value fallback |
T |
*T |
Address-of helper |
When a field is a named struct, goxmap generates a sub-mapper and calls it:
// User has *Address, UserDTO has *AddressDTO
func MapUserToUserDTO(src *User) *UserDTO {
// ...
dst.Address = func() *AddressDTO {
if src.Address != nil {
return MapAddressToAddressDTO(src.Address)
}
return nil
}()
// ...
}If MapAddressToAddressDTO already exists in your package (hand-written), goxmap detects it and reuses it instead of generating a new one.
Slices of structs are mapped with a nil-checked loop within pointer-safe functions:
func MapUserToUserDTO(src *User) *UserDTO {
if src == nil { return nil }
dst := &UserDTO{}
if src.Emails != nil {
dst.Emails = make([]*EmailInfoDTO, len(src.Emails))
for i, v := range src.Emails {
dst.Emails[i] = MapEmailInfoToEmailInfoDTO(v)
}
}
return dst
}Nil slices remain nil. Empty slices remain empty.
Widening conversions (no data loss) are handled automatically:
dst.BigID = int64(src.SmallID) // int32 -> int64 (safe)
dst.Score = float64(src.Score) // float32 -> float64 (safe)Narrowing conversions (potential data loss) produce a compile-time error:
goxmap: error: field UserDTO.Age has narrowing conversion: int64 -> int32.
This may lose data. To allow this, add a converter function:
func MapInt64ToInt32(v int64) int32 { return int32(v) }
This prevents silent truncation bugs. If you intentionally want the narrowing cast, add an explicit converter function — one line makes it clear you've considered the risk.
| Conversion | Category | Behavior |
|---|---|---|
int32 → int64 |
Widening | Auto-cast |
float32 → float64 |
Widening | Auto-cast |
int64 → int32 |
Narrowing | Error (add converter) |
float64 → float32 |
Narrowing | Error (add converter) |
int → uint |
Cross-sign | Error (add converter) |
int → int32 |
Narrowing | Error (platform-dependent) |
For pointer-to-pointer combinations with type coercion, nil safety is maintained:
// *int32 to *int64: pointer-based with type conversion
dst.BigID = func() *int64 {
if src.SmallID != nil {
v := int64(*src.SmallID)
return &v
}
return nil
}()All Go numeric types are supported: int, int8-int64, uint, uint8-uint64, float32, float64, byte, rune.
When source and destination fields use different named types with the same underlying type, goxmap generates a direct type conversion:
type StatusA string
type StatusB string
// Generated:
dst.Status = StatusB(src.Status)This covers Go enum patterns (string const types, iota int types). For custom value mapping between incompatible types, use mapper:"func:...".
When types don't match and no numeric cast applies, goxmap searches the package for a function named Map<SrcType>To<DstType>. No mapper:"func:..." tag needed if you follow the naming convention.
// goxmap finds this automatically when mapping time.Time -> string
func MapTimeToString(v time.Time) string {
return v.Format(time.RFC3339)
}If no converter function exists and types are incompatible, the generator fails with a helpful error:
goxmap: error: field EventDTO.CreatedAt has type mismatch: time.Time -> string.
No converter function MapTimeToString found in package.
Add a function: func MapTimeToString(v time.Time) string { ... }
The mapper:"func:FnName" tag still works and takes priority over auto-discovery for cases where you want a non-standard function name.
Generate both MapAToB and MapBToA with a single command using -bidi:
go run github.com/hacks1ash/goxmap -src User -dst UserDTO -bidiThis generates both directions in the same file. Works in both same-package and cross-package modes.
Use the mapper:"func:FnName" tag to override the auto-discovery convention for a specific field:
type Event struct {
CreatedAt time.Time
}
type EventDTO struct {
CreatedAt string `mapper:"func:FormatTime"`
}
func FormatTime(t time.Time) string {
return t.Format(time.RFC3339)
}Map to and from external packages using bind tags:
type User struct {
Name string `mapper:"bind:FullName"`
Email string `mapper:"bind:UserEmail"`
}go run github.com/hacks1ash/goxmap \
-src User -dst ExternalUser \
-external-pkg github.com/org/repo/proto \
-bidiThis generates both MapUserToExternalUser and MapExternalUserToUser, using Protobuf-style GetFullName() getters in the reverse direction for nil safety.
| Tag | Scope | Example | Description |
|---|---|---|---|
mapper:"func:Fn" |
Field | mapper:"func:FormatTime" |
Call Fn(src.Field) for this field |
mapper:"bind:Name" |
Field | mapper:"bind:UserId" |
Match to external field by Go name |
mapper:"bind_json:key" |
Field | mapper:"bind_json:user_id" |
Match to external field by its json tag |
mapper:"struct_func:Fn" |
Struct | mapper:"struct_func:Custom" |
Delegate entire struct to Fn |
mapper:"ignore" |
Field | mapper:"ignore" |
Exclude field from mapping entirely |
mapper:"optional" |
Field | mapper:"optional" |
Suppress warning if no source match |
Tags can be combined with ;: mapper:"bind:FullName;func:Trim"
goxmap [flags]
Flags:
-src Source struct type name (required)
-dst Destination struct type name (required)
-func Generated function name (default: Map<Src>To<Dst>)
-dir Package directory (default: $GOFILE dir or ".")
-output Output file name (default: <dst_snake>_mapper_gen.go)
-struct-func Delegate entire mapping to this function
-external-pkg Import path for cross-package mapping
-bidi Generate bidirectional mappers (same-package or cross-package)
See the _examples/ directory for self-contained, runnable examples:
| Example | Description |
|---|---|
01-standard-dto |
Database model to JSON DTO with pointer handling |
02-protobuf-integration |
Cross-package mapping with bind tags and getter detection |
03-custom-converters |
Custom functions for time.Time formatting and computed fields |
- Technical Design -- Architecture, algorithms, and design decisions
- Performance -- Benchmark methodology and results
See CONTRIBUTING.md for development setup, testing, and contribution guidelines.