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 is

View on GitHub (pinned to 41dac167fd)

Solutions

  1. Use a plain struct without a Scan method as the StructScan destination.
  2. Remove the Scan method if the type is meant to be a row struct, not a column type.
  3. 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

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


AI-assisted analysis of jmoiron/sqlx@41dac167fd (2026-09-03). Data as JSON: /api/errors/978f267c43a08507. Report an issue: GitHub.