{"record":{"id":"978f267c43a08507","repo":"jmoiron/sqlx","slug":"structscan-expects-a-struct-dest-but-the-provided","errorCode":null,"errorMessage":"structscan expects a struct dest but the provided struct type %s implements scanner","messagePattern":"structscan expects a struct dest but the provided struct type (.+?) implements scanner","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sqlx.go","lineNumber":878,"sourceCode":"\ntype rowsi interface {\n\tClose() error\n\tColumns() ([]string, error)\n\tErr() error\n\tNext() bool\n\tScan(...interface{}) error\n}\n\n// structOnlyError returns an error appropriate for type when a non-scannable\n// struct is expected but something else is given\nfunc structOnlyError(t reflect.Type) error {\n\tisStruct := t.Kind() == reflect.Struct\n\tisScanner := reflect.PtrTo(t).Implements(_scannerInterface)\n\tif !isStruct {\n\t\treturn fmt.Errorf(\"expected %s but got %s\", reflect.Struct, t.Kind())\n\t}\n\tif isScanner {\n\t\treturn fmt.Errorf(\"structscan expects a struct dest but the provided struct type %s implements scanner\", t.Name())\n\t}\n\treturn fmt.Errorf(\"expected a struct, but struct %s has no exported fields\", t.Name())\n}\n\n// scanAll scans all rows into a destination, which must be a slice of any\n// type.  It resets the slice length to zero before appending each element to\n// the slice.  If the destination slice type is a Struct, then StructScan will\n// be used on each row.  If the destination is some other kind of base type,\n// then each row must only have one column which can scan into that type.  This\n// allows you to do something like:\n//\n//\trows, _ := db.Query(\"select id from people;\")\n//\tvar ids []int\n//\tscanAll(rows, &ids, false)\n//\n// and ids will be a list of the id results.  I realize that this is a desirable\n// interface to expose to users, but for now it will only be exposed via changes\n// to `Get` and `Select`.  The reason that this has been implemented like this is","sourceCodeStart":860,"sourceCodeEnd":896,"githubUrl":"https://github.com/jmoiron/sqlx/blob/41dac167fdad5e3fd81d66cafba0951dc6823a30/sqlx.go#L860-L896","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\ntype User struct{ ... }\nfunc (u *User) Scan(src interface{}) error { ... } // blocks StructScan\nrows.StructScan(&u)\n\n// after\n// remove Scan from User, or scan single-column:\nrows.Scan(&u)","handlingStrategy":"type-guard","validationCode":"t := reflect.TypeOf(dest).Elem()\nif reflect.PtrTo(t).Implements(reflect.TypeOf((*sql.Scanner)(nil)).Elem()) {\n    return errors.New(\"dest implements sql.Scanner; use rows.Scan, not StructScan\")\n}","typeGuard":"func isPlainStruct(dest interface{}) bool {\n    t := reflect.TypeOf(dest)\n    if t == nil || t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {\n        return false\n    }\n    e := t.Elem()\n    return !reflect.PtrTo(e).Implements(reflect.TypeOf((*sql.Scanner)(nil)).Elem())\n}","tryCatchPattern":"if err := rows.StructScan(&row); err != nil {\n    if strings.Contains(err.Error(), \"implements scanner\") {\n        return rows.Scan(&row) // single-column fallback\n    }\n    return err\n}","preventionTips":["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."],"tags":["sqlx","struct-scan","sql-scanner","type-mismatch"],"backgroundTag":"scan-dest-type-mismatch","analyzedSha":"41dac167fdad5e3fd81d66cafba0951dc6823a30","analyzedAt":"2026-09-03T06:10:20.382Z","contentChangedAt":"2026-09-03T06:10:20.382Z","schemaVersion":2},"datasetVersion":"2026-09-10T12:17:11.382Z"}