README
ยถ
Automapper Generator
An easy-to-use CLI utility that generates reflection-free Go 1.18+ code for mapping structs with type conversion support.
โ ๏ธ Development Status: This project is in early development. There are yet no integrated tests, and some things may be subject to change. Any feedback is greatly appreciated!
Features
- ๐ Zero Reflection: Generates type-safe code at compile time
- ๐ฆ Remote Packages: Map from structs in any Go module (local or remote)
- ๐ Module Cache Support: Automatically loads types from Go's module cache
- ๐ Type Conversion: Convert any field types or formats via custom functions
- ๐ท๏ธ Flexible Mapping: Tag-based field mapping and transformations
- ๐ชบ Nesting: Nested models can be automatically handled
- ๐ Validation: Prevents generation of broken code, predicts problems, suggests solutions
- โก Performance: Direct field assignments, no runtime overhead
Table of Contents
How It Works
- Parse Configuration: Reads
automapper.jsonto understand the project setup - Parse Source Code: Uses Go's AST parser to analyze structs and fields
- Extract Annotations: Finds
// automapper:from=...comments on DTOs - Match Fields: Maps source fields to target fields using name transforms and tags
- Generate Code: Uses jennifer to generate type-safe Go code
- Write Output: Creates the mapper file with all MapFrom methods
Generated Code Structure
/*
Code generated by automapper-gen. DO NOT EDIT.
Learn more: https://git.weirdcat.su/weirdcat/automapper-gen
*/
package dtos
import (
"errors"
"fmt"
db "git.weirdcat.su/weirdcat/automapper-gen/example/db"
)
// MapFromUserDB maps from db.UserDB to UserDTO
func (d *UserDTO) MapFromUserDB(src *db.UserDB) error {
if src == nil {
return errors.New("source is nil")
}
d.ID = src.ID
d.Username = src.Username
{
var err error
d.Role, err = StrRoleToEnum(src.Role)
if err != nil {
return fmt.Errorf("converting field Role: %w", err)
}
}
if src.About != nil {
d.About = *src.About
}
// About: nil pointer will result in zero value
{
d.Pets = make([]PetDTO, len(src.Pets))
for i, item := range src.Pets {
var err error
err = d.Pets[i].MapFromPetDB(&item)
if err != nil {
return fmt.Errorf("mapping nested field Pets[%d]: %w", i, err)
}
}
}
if src.FeaturedAchievement != nil {
var nested AchievementDTO
var err error
err = nested.MapFromAchievementDB(src.FeaturedAchievement)
if err != nil {
return fmt.Errorf("mapping nested field FeaturedAchievement: %w", err)
}
d.FeaturedAchievement = nested
}
// FeaturedAchievement: nil pointer will result in zero value
{
var err error
d.Interests, err = StrInterestsToEnums(src.Interests)
if err != nil {
return fmt.Errorf("converting field Interests: %w", err)
}
}
if src.Birthday != nil {
result := TimeToJSString(*src.Birthday)
d.Birthday = &result
}
// Birthday: nil pointer will result in nil
d.CreatedAt = TimeToJSString(src.CreatedAt)
return nil
}
// MapFromPetDB maps from db.PetDB to PetDTO
func (d *PetDTO) MapFromPetDB(src *db.PetDB) error {
if src == nil {
return errors.New("source is nil")
}
d.ID = src.ID
d.Name = src.Name
{
var err error
d.Interests, err = StrInterestsToEnums(src.Interests)
if err != nil {
return fmt.Errorf("converting field Interests: %w", err)
}
}
if src.Birthday != nil {
result := TimeToJSString(*src.Birthday)
d.Birthday = &result
}
// Birthday: nil pointer will result in nil
d.CreatedAt = TimeToJSString(src.CreatedAt)
return nil
}
// MapFromAchievementDB maps from db.AchievementDB to AchievementDTO
func (d *AchievementDTO) MapFromAchievementDB(src *db.AchievementDB) error {
if src == nil {
return errors.New("source is nil")
}
d.ID = src.ID
d.Title = src.Title
d.Description = ToLower(src.Description)
return nil
}
Installation
From Source
# Clone the repository
git clone https://git.weirdcat.su/weirdcat/automapper-gen.git
cd automapper-gen
# Build and install
make install
# Or build only
make build
Using Go Install
go install git.weirdcat.su/weirdcat/automapper-gen/cmd/automapper-gen@latest
Quick Start
1. Create Configuration
Head to the directory where the destination structs or DTOs are supposed to be.
Create an automapper.json:
{
"output": "automappers.go",
"converters": [],
"externalPackages": [
{
"alias": "db",
"importPath": "git.weirdcat.su/weirdcat/automapper-gen/example/db",
"localPath": "../db"
}
]
}
Note that since your source structs (e.g. database models) are most likely in a different package, we have to specify the package in externalPackages. In this example we use a database package from the same go module which is located in ./example/db. The alias option helps to avoid name collisions in the generated file when there are multiple external packages.
You may also use the localPath parameter which overrides the importPath. This may be useful if the generator is unable to discover a local package otherwise.
Note: External packages are normally loaded directly from Go's module cache. The package simply needs to be installed and added to externalPackages via the importPath.
2. Define Your Structs
Database Model (db/models.go):
package db
import "time"
type UserDB struct {
ID int64
Username string
Email string
Password string
}
This is what the generator is going to map from.
DTO Model (dtos/user.go):
package dtos
//automapper:from=db.UserDB
type UserDTO struct {
ID int64
Username string
Email string
}
This is the destination struct that the generator will attempt to map the previous struct to.
The database model requires none of our tags or annotations. This design choice was made for the sake of compatibility with database model generators that would otherwise overwrite the extra information. This makes the tool work especially well with the SQL-compiler sqlc.
3. Generate Mappers
Simply run the command in the second directory:
# From the dtos directory
automapper-gen .
Great, the code has been generated and written into the automappers.go file in the same directory. The file now declares a method for our UserDTO destination struct, which will map data from the source struct db.UserDB:
(d* UserDTO) MapFromUserDB(src* db.UserDB)
the mapper methods always follow the pattern obj.MapFromT(src T).
Automapper Generator utilizes its own validation system to find errors and warn the user about potential problems. However, if it fails and still generates faulty code, we are still safe, since the code will not compile and the problem will be noticed immediately, unlike with reflection-based mappers.
4. Use the Generated Code
Usage of the generated code is straightforward:
package main
import (
"fmt"
"time"
"yourproject/db"
"yourproject/dtos"
)
func main() {
// Source data
user := &db.UserDB{
ID: 1,
Username: "john_doe",
Email: "john@example.com",
Password: "hashed_password",
}
// Map to DTO
dto := &dtos.UserDTO{}
if err := dto.MapFromUserDB(user); err != nil {
panic(err)
}
fmt.Printf("User: %+v\n", dto)
// Output: User: {ID:1 Username:john_doe Email:john@example.com}
}
Configuration
Configuration File (automapper.json)
| Field | Type | Required | Description |
|---|---|---|---|
output |
string | No | Output filename (default: "automappers.go") |
converters |
array | No | List converters with name and function |
nilPointersForNull |
bool | No | Use nil pointers for null values |
externalPackages |
array | No | External packages to parse |
External Packages
External packages are loaded directly from Go's module cache, making it easy to map from types in any Go module:
{
"externalPackages": [
{
"alias": "db",
"importPath": "github.com/yourorg/project/db"
},
{
"alias": "models",
"importPath": "git.example.com/team/service/models"
}
]
}
Local Development: If you're working on a module locally and want to use local changes:
{
"externalPackages": [
{
"alias": "db",
"importPath": "github.com/yourorg/project/db",
"localPath": "../db"
}
]
}
The generator will try the local path first, then fall back to the module cache.
Usage
Remote Modules
One of the key features is the ability to map from types in any Go module, whether it's in your repository or a completely separate one:
Example: Mapping from a Different Repository
Repository 1 (git.weirdcat.su/test/prj1):
// bd/models.go
package bd
import "time"
type User struct {
ID int64
Username string
CreatedAt time.Time
}
Repository 2 (git.weirdcat.su/test/prj2):
// dto/automapper.json
{
"output": "automappers.go",
"externalPackages": [
{
"alias": "db",
"importPath": "git.weirdcat.su/test/prj1/bd"
}
]
}
// dto/user.go
package dto
//automapper:from=db.User
type UserDTO struct {
ID int64
Username string
CreatedAt string `automapper:"converter=TimeToJSString"`
}
Prerequisites: Make sure the external module is in your go.mod:
go get git.weirdcat.su/test/prj1
Then generate:
cd dto
automapper-gen .
The generator will load the bd package from your module cache and generate the appropriate mappers!
Basic Mapping
//automapper:from=SourceStruct
type TargetDTO struct {
Field1 string
Field2 int
}
Multiple Source Structs
It is possible to specify multiple source structs in the automapper:from annotation:
//automapper:from=User,Profile
type UserProfileDTO struct {
// Will generate MapFromUser and MapFromProfile
Name string
Email string
}
Two seperate methods will be created. This is useful when there are two database requests that return different objects with identical or similar contents.
Field Tags
Skip Field
type UserDTO struct {
Password string `automapper:"-"` // Will not be mapped
}
Custom Field Mapping
We are able to handle field name mismatches using the field parameter:
type UserDTO struct {
Name string `automapper:"field=Full_Name"` // Maps from Full_Name
}
Field Converter
More info the converters section.
type UserDTO struct {
CreatedAt string `automapper:"converter=TimeToJSString"`
}
Combined Tags
type UserDTO struct {
BirthDate string `automapper:"field=Birthday,converter=TimeToJSString"`
}
Converters
We often have to deal with mismatching data types or want to adjust the format of the data. We can automate conversion by implementing custom converters in the package to do the work for us.
In the same package where we store the destination structs, create a new .go file.
We suggest the name converters.go. Write one or several conversion functions:
package dtos
import (
"fmt"
"git.weirdcat.su/weirdcat/automapper-gen/example/types"
)
// Regular converter
func StrRoleToEnum(role string) (types.Role, error) {
switch role {
case "admin":
return types.RoleAdmin, nil
case "user":
return types.RoleUser, nil
default:
return types.RoleGuest, fmt.Errorf("unknown role: %s", role)
}
}
// Safe converter
func ToLower(s string) string {
return strings.ToLower(s)
}
Note: Converter functions must follow the signature func(T) (U, error) or func(T) (U) and be in the same package as your DTOs.
Update your automapper.json to include your converters:
{
"output": "automappers.go",
"converters": [
{
"name": "RoleEnum",
"function": "StrRoleToEnum"
},
{
"name": "ToLower",
"function": "Lowercase"
}
],
"externalPackages": [
{
"alias": "db",
"importPath": "git.weirdcat.su/weirdcat/automapper-gen/example/db"
}
]
}
The name parameter defines the tag which will be used in our destination structs to mark the conversion
strategy for a given field.
The function parameter references the name of the function that we created. The function will be tied to the previously
defined tag name.
Use in your DTOs:
type UserDTO struct {
Role Role `automapper:"converter=RoleEnum"`
Bio string `automapper:"converter=LowerCase"`
}
Nested Structs
The nested struct feature allows automatic mapping of complex nested structures without manual field-by-field copying. When a source struct contains fields that should map to other DTOs, you can use the dto tag to trigger automatic nested mapping:
type UserDTO struct {
ID int64
Username string
Pets []PetDTO `automapper:"dto=PetDTO"`
FeaturedAchievement AchievementDTO `automapper:"dto=AchievementDTO"`
}
//automapper:from=db.PetDB
type PetDTO struct {
ID int64
Name string
}
//automapper:from=db.AchievementDB
type AchievementDTO struct {
ID int64
Title string
}
The generator will recursively handle the nested structures and generate code that reuses
the MapFrom methods for the inner structs, so that mapping, convertion and validation will
run normally.
The dto tag can also be combined with other automapper options:
// Just nested DTO
Field DTO `automapper:"dto=TargetDTO"`
// Nested DTO with custom field mapping
Field DTO `automapper:"dto=TargetDTO,field=SourceFieldName"`
// Cannot combine dto with converter (dto takes precedence)
