Edge Cases
Understand how mapper handles null values, missing columns, extra columns, type conversions, pointers, JSON fields, and row count errors.
Mapper is designed to handle common database scanning edge cases without requiring manual scan logic in every query.
This page summarizes how mapper behaves when values are missing, nullable, extra, converted, or not assignable.
Overview
| Case | Behavior |
|---|---|
NULL values | Assigned to pointers, nullable structs, maps, or left as zero value where applicable |
| Extra columns | Ignored when no matching struct field exists |
| Missing columns | Matching struct fields keep their zero value |
| Type conversions | Common numeric, string, boolean, time, slice, and map conversions are handled by AssignValue |
| Pointer fields | Mapper allocates and assigns pointer values when the source value is not NULL |
| JSON fields | JSON strings or byte slices can be assigned into slices and maps |
| Empty result | ScanStructSlice returns an empty slice, ScanStructOne returns ErrNoRows |
| Too many rows | ScanStructOne returns ErrTooManyRows |
| Invalid assignment | Mapper returns an error when a value cannot be assigned to the target field |
Null Values
When a database value is NULL, mapper handles it according to the target field type.
type User struct {
ID string `db:"id"`
Email *string `db:"email"`
}If email is NULL, the Email field remains nil.
If email contains a value, mapper allocates the pointer and assigns the value.
if user.Email != nil {
fmt.Println(*user.Email)
}For non-pointer primitive fields, a NULL value leaves the field unchanged, usually its zero value.
type User struct {
ID string `db:"id"`
Active bool `db:"active"`
}If active is NULL, the field remains false.
Nullable Structs
Mapper supports nullable-style structs with a Valid field and a supported value field.
Supported value field names are:
StringTimeBoolInt64Float64
Example:
type NullString struct {
String string
Valid bool
}
type User struct {
ID string `db:"id"`
Email NullString `db:"email"`
}If email is NULL, the nullable struct is reset to its zero value.
If email contains a value, mapper assigns the value and sets Valid to true.
if user.Email.Valid {
fmt.Println(user.Email.String)
}This works with nullable structs that follow the same field shape, including standard-library-like nullable types.
Extra Columns
Extra columns are ignored when no matching struct field exists.
type User struct {
ID string `db:"id"`
Name string `db:"name"`
}Query:
SELECT id, name, created_at FROM usersThe created_at column is ignored because the struct does not define a matching field.
This makes mapper safe to use with:
SELECT *- joined queries
- aliased queries
- queries that return additional computed values
Missing Columns
If a result set does not contain a column for a struct field, that field keeps its zero value.
type User struct {
ID string `db:"id"`
Name string `db:"name"`
Active bool `db:"active"`
}Query:
SELECT id, name FROM usersThe Active field is not present in the result set, so it remains false.
This behavior is useful for partial queries and read models.
Type Conversions
Mapper uses AssignValue to assign scanned values into struct fields.
It supports common conversions between database values and Go types.
Examples include:
| Source value | Target field |
|---|---|
[]byte | string |
string | string |
| numeric values | int, int64, uint, float64, and related numeric types |
bool | bool |
time.Time | time.Time |
string or []byte JSON | slices and maps |
non-NULL values | pointer fields |
Example:
type User struct {
ID int64 `db:"id"`
Name string `db:"name"`
CreatedAt time.Time `db:"created_at"`
}If a value cannot be converted safely, mapper returns an error.
Boolean Values
Boolean assignment depends on the API being used.
When assigning directly into struct fields, mapper expects a real boolean value for bool fields.
type User struct {
Active bool `db:"active"`
}For dynamic row maps, the helper functions are more flexible.
active, ok := mapper.AsBool(row["active"])AsBool accepts booleans, numeric values, and common string values such as:
truefalse10yesno
Pointer Fields
Pointer fields are useful for optional database values.
type User struct {
ID string `db:"id"`
Email *string `db:"email"`
DeletedAt *time.Time `db:"deleted_at"`
}When the source value is not NULL, mapper creates and assigns the pointer value.
When the source value is NULL, the pointer remains nil.
if user.DeletedAt == nil {
fmt.Println("not deleted")
}JSON Fields
Mapper can assign JSON strings or byte slices into slice and map fields.
type User struct {
ID string `db:"id"`
Tags []string `db:"tags"`
Metadata map[string]string `db:"metadata"`
}The database driver must return the JSON value as a string or []byte.
Example database values:
["admin", "active"]{"source":"import","role":"admin"}Mapper unmarshals the JSON into the target slice or map.
If the JSON is invalid, mapper returns an error.
Empty Results
ScanStructSlice returns an empty slice when there are no rows.
users, err := mapper.ScanStructSlice[User](rows)
if err != nil {
return err
}
fmt.Println(len(users)) // 0ScanStructRows simply does not call the callback when there are no rows.
err := mapper.ScanStructRows[User](rows, func(user *User) error {
fmt.Println(user.Name)
return nil
})ScanStructOne returns mapper.ErrNoRows.
user, err := mapper.ScanStructOne[User](rows)
if errors.Is(err, mapper.ErrNoRows) {
return nil
}
if err != nil {
return err
}Too Many Rows
ScanStructOne expects exactly one row.
If more than one row is returned, it returns mapper.ErrTooManyRows.
user, err := mapper.ScanStructOne[User](rows)
if errors.Is(err, mapper.ErrTooManyRows) {
return errors.New("expected only one user")
}
if err != nil {
return err
}
fmt.Println(user.Name)Use ScanStructSlice when multiple rows are expected.
Invalid Assignment
Mapper returns an error when a source value cannot be assigned to the target field.
Example:
type User struct {
CreatedAt time.Time `db:"created_at"`
}If the database returns a non-time value that cannot be assigned to time.Time, mapper returns an error.
This helps catch schema mismatches early.
Recommended Practices
Use explicit db tags for stable mapping.
type User struct {
ID string `db:"id"`
Name string `db:"name"`
CreatedAt time.Time `db:"created_at"`
}Use pointer fields or nullable structs for optional database values.
type User struct {
Email *string `db:"email"`
}Use SQL aliases for joins and computed columns.
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_idUse ScanStructOne only when the query is expected to return exactly one row.
Use ScanStructSlice or ScanStructRows when multiple rows are expected.