Skip to content

hacks1ash/goxmap

Repository files navigation

goxmap

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 UserDTO

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


Why goxmap?

Compile-Time Safety vs. Runtime Panics

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

Zero Dependencies in Generated Code

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.

Performance

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

Quick Start

1. Install

go install github.com/hacks1ash/goxmap@latest

Or use it as a tool dependency (recommended for reproducible builds):

go get -tool github.com/hacks1ash/goxmap

2. Define your structs

package 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"`
}

3. Add a generate directive

//go:generate go run github.com/hacks1ash/goxmap -src User -dst UserDTO

4. Run

go 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
}

Features

Automatic Pointer Handling

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

Recursive Nested Mapping

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.

Slice Support

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.

Automatic Numeric Type Coercion

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
int32int64 Widening Auto-cast
float32float64 Widening Auto-cast
int64int32 Narrowing Error (add converter)
float64float32 Narrowing Error (add converter)
intuint Cross-sign Error (add converter)
intint32 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.

Named Type Conversion (Enum Support)

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:...".

Automatic Converter Discovery

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.

Bidirectional Mapping

Generate both MapAToB and MapBToA with a single command using -bidi:

go run github.com/hacks1ash/goxmap -src User -dst UserDTO -bidi

This generates both directions in the same file. Works in both same-package and cross-package modes.

Custom Converter Functions

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)
}

Cross-Package Mapping (Protobuf, OpenAPI)

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 \
    -bidi

This generates both MapUserToExternalUser and MapExternalUserToUser, using Protobuf-style GetFullName() getters in the reverse direction for nil safety.

Tag Reference

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"


CLI Reference

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)

Examples

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

Documentation


Contributing

See CONTRIBUTING.md for development setup, testing, and contribution guidelines.


License

MIT

About

goxmap is a compile-time code generator that produces type-safe, zero-dependency struct mapping functions for Go.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages