jmoiron/sqlx · error
structscan expects a struct dest but the provided struct typ
Error message
structscan expects a struct dest but the provided struct type %s implements scanner
What it means
structOnlyError detects that the provided destination struct (or its pointer type) implements sql.Scanner. Such a type would normally be scanned as a single column value, but StructScan expects a plain struct to map columns onto fields; the conflict is reported because column-to-field mapping is meaningless for a Scanner type.
Source
Thrown at sqlx.go:878
type rowsi interface {
Close() error
Columns() ([]string, error)
Err() error
Next() bool
Scan(...interface{}) error
}
// structOnlyError returns an error appropriate for type when a non-scannable
// struct is expected but something else is given
func structOnlyError(t reflect.Type) error {
isStruct := t.Kind() == reflect.Struct
isScanner := reflect.PtrTo(t).Implements(_scannerInterface)
if !isStruct {
return fmt.Errorf("expected %s but got %s", reflect.Struct, t.Kind())
}
if isScanner {
return fmt.Errorf("structscan expects a struct dest but the provided struct type %s implements scanner", t.Name())
}
return fmt.Errorf("expected a struct, but struct %s has no exported fields", t.Name())
}
// scanAll scans all rows into a destination, which must be a slice of any
// type. It resets the slice length to zero before appending each element to
// the slice. If the destination slice type is a Struct, then StructScan will
// be used on each row. If the destination is some other kind of base type,
// then each row must only have one column which can scan into that type. This
// allows you to do something like:
//
// rows, _ := db.Query("select id from people;")
// var ids []int
// scanAll(rows, &ids, false)
//
// and ids will be a list of the id results. I realize that this is a desirable
// interface to expose to users, but for now it will only be exposed via changes
// to `Get` and `Select`. The reason that this has been implemented like this isView on GitHub (pinned to 41dac167fd)
Solutions
- Use a plain struct without a Scan method as the StructScan destination.
- Remove the Scan method if the type is meant to be a row struct, not a column type.
- Scan the row as a single column with rows.Scan(&customType) instead of StructScan.
Example fix
// before
type User struct{ ... }
func (u *User) Scan(src interface{}) error { ... } // blocks StructScan
rows.StructScan(&u)
// after
// remove Scan from User, or scan single-column:
rows.Scan(&u) Defensive patterns
Strategy: type-guard
Validate before calling
t := reflect.TypeOf(dest).Elem()
if reflect.PtrTo(t).Implements(reflect.TypeOf((*sql.Scanner)(nil)).Elem()) {
return errors.New("dest implements sql.Scanner; use rows.Scan, not StructScan")
} Type guard
func isPlainStruct(dest interface{}) bool {
t := reflect.TypeOf(dest)
if t == nil || t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
return false
}
e := t.Elem()
return !reflect.PtrTo(e).Implements(reflect.TypeOf((*sql.Scanner)(nil)).Elem())
} Try / catch
if err := rows.StructScan(&row); err != nil {
if strings.Contains(err.Error(), "implements scanner") {
return rows.Scan(&row) // single-column fallback
}
return err
} Prevention
- Reserve Scan/Value methods for column types, never for row models.
- Keep row structs free of sql.Scanner implementations.
- Document custom types so teams don't attach Scan to models.
When it happens
Trigger: Calling StructScan with a dest struct whose *T implements sql.Scanner (e.g. a custom type with a Scan method, like wrappers around JSON columns).
Common situations: Reusing a custom column type (e.g. type Tags json.RawMessage with Scan/Value) as a full-row destination; accidentally implementing Scan on an entire model struct.
Related errors
- expected %s but got %s
- missing destination name %s in %T
- expected a struct, but struct %s has no exported fields
- non-statement type %v passed to Stmtx
- non-statement type %v passed to Stmtx
AI-assisted analysis of jmoiron/sqlx@41dac167fd (2026-09-03).
Data as JSON: /api/errors/978f267c43a08507.
Report an issue: GitHub.