Dynamic Rows
Work with dynamic row maps, typed row helpers, and manual struct mapping.
Mapper is most commonly used to scan database rows into structs.
For dynamic use cases, it can also scan rows into map[string]any, fill structs from maps, or provide typed access to row values.
This is useful when:
- the result shape is dynamic
- you do not want to define a struct for every query
- you are working with joins, reports, exports, or admin tooling
- you need custom mapping logic
- you want to inspect raw database values before assigning them
Scan Rows Into Maps
Use ScanMapRows when you want each database row as a map[string]any.
rows, err := db.Query(`
SELECT *
FROM users
`)
if err != nil {
return err
}
defer rows.Close()
err = mapper.ScanMapRows(rows, func(row map[string]any) error {
fmt.Println(row["id"], row["name"])
return nil
})
if err != nil {
return err
}Each returned row is represented as a map where:
- the key is the database column name
- the value is the scanned database value
Example result:
map[string]any{
"id": "u_123",
"name": "Alice",
"email": "alice@example.com",
"active": true,
}When to Use Maps
Maps are useful when the result does not have a stable struct shape.
For example:
rows, err := db.Query(`
SELECT status, COUNT(*) AS total
FROM users
GROUP BY status
`)You can process the result dynamically:
err = mapper.ScanMapRows(rows, func(row map[string]any) error {
status := row["status"]
total := row["total"]
fmt.Println(status, total)
return nil
})For stable application models, structs are usually preferred because they provide stronger type safety.
The Row Type
Row is a convenience type for working with map[string]any.
type Row map[string]anyIt provides typed helper methods for common values.
row := mapper.Row{
"id": int64(123),
"name": "Alice",
"active": true,
}You can read values using typed accessors:
id, ok := row.Int64("id")
if !ok {
return errors.New("invalid id")
}
name, ok := row.String("name")
if !ok {
return errors.New("invalid name")
}
active, ok := row.Bool("active")
if !ok {
return errors.New("invalid active value")
}Row Helpers
The Row type provides these helper methods:
| Method | Purpose |
|---|---|
Int | Read a value as int |
Int64 | Read a value as int64 |
String | Read a value as string |
Bool | Read a value as bool |
Time | Read a value as time.Time |
Example:
createdAt, ok := row.Time("created_at")
if !ok {
return errors.New("invalid created_at value")
}Each helper returns two values:
value, ok := row.String("name")If the value can be converted, ok is true.
If the value is missing or cannot be converted, ok is false.
Standalone Converters
The same conversions are also available as standalone functions.
name, ok := mapper.AsString(row["name"])
active, ok := mapper.AsBool(row["active"])
createdAt, ok := mapper.AsTime(row["created_at"])Available converters:
| Function | Purpose |
|---|---|
AsInt | Convert a value to int |
AsInt64 | Convert a value to int64 |
AsString | Convert a value to string |
AsBool | Convert a value to bool |
AsTime | Convert a value to time.Time |
These helpers are useful when working with raw row maps or custom mapping logic.
Fill a Struct From a Map
Use FillFromMap when you already have a map[string]any and want to fill a struct.
type User struct {
ID int64 `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
Active bool `db:"active"`
}
row := map[string]any{
"id": int64(1),
"name": "Alice",
"email": "alice@example.com",
"active": true,
}
var user User
err := mapper.FillFromMap(&user, row)
if err != nil {
return err
}The same mapping rules are used as with row scanning:
dbtagjsontag- Go field name
- snake_case fallback
Maps and Column Names
When scanning rows into maps, mapper uses the column names returned by the database driver.
This means SQL aliases become map keys.
rows, err := db.Query(`
SELECT
u.id AS user_id,
u.name AS user_name,
r.name AS role_name
FROM users u
JOIN roles r ON r.id = u.role_id
`)The row map will contain keys such as:
map[string]any{
"user_id": "u_123",
"user_name": "Alice",
"role_name": "Admin",
}Aliases are recommended when joining tables that contain columns with the same name.
Custom Mapping With ScanMapper
For advanced cases, a struct can implement the ScanMapper interface.
type User struct {
ID string
Name string
}
func (u *User) ScanMap(row map[string]any) error {
id, ok := mapper.AsString(row["id"])
if !ok {
return errors.New("invalid id")
}
name, ok := mapper.AsString(row["name"])
if !ok {
return errors.New("invalid name")
}
u.ID = id
u.Name = name
return nil
}This gives you full control over how a row map is converted into a struct.
Custom mapping is useful when:
- column names do not match struct fields
- values need custom parsing
- multiple columns should be combined into one field
- fallback values are needed
- validation should happen during mapping
Structs vs Maps
Use structs when the result shape is known.
users, err := mapper.ScanStructSlice[User](rows)Use maps when the result shape is dynamic.
err = mapper.ScanMapRows(rows, func(row map[string]any) error {
fmt.Println(row)
return nil
})In most application code, structs are preferred.
Maps are better for generic tooling, reports, exports, debugging, and custom data processing.
Recommended Usage
For regular application queries:
users, err := mapper.ScanStructSlice[User](rows)For dynamic query results:
err = mapper.ScanMapRows(rows, func(row map[string]any) error {
fmt.Println(row)
return nil
})For manual mapping:
var user User
err := mapper.FillFromMap(&user, row)
if err != nil {
return err
}For custom parsing and validation, implement ScanMapper.