jmoiron/sqlx · error
expected a struct, but struct %s has no exported fields
Error message
expected a struct, but struct %s has no exported fields
What it means
structOnlyError fires this when the destination is a struct kind but has no exported fields, so the mapper has nothing to bind columns to. Unexported fields are invisible to reflection-based mapping, making StructScan impossible.
Source
Thrown at sqlx.go:880
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
// this is the only way to not duplicate reflect work in the new API while
// maintaining backwards compatibility.View on GitHub (pinned to 41dac167fd)
Solutions
- Export the struct fields you want scanned (capitalize them) and add db tags.
- Use a different struct with exported fields as the scan destination.
- Add at least one exported field if the struct is intentionally blank.
Example fix
// before
type user struct { name string }
db.Get(&u, "SELECT name FROM users")
// after
type User struct { Name string `db:"name"` }
db.Get(&u, "SELECT name FROM users") Defensive patterns
Strategy: type-guard
Validate before calling
t := reflect.TypeOf(dest).Elem()
if t.NumField() == 0 || !hasExportedField(t) {
return errors.New("struct dest has no exported fields")
} Type guard
func hasExportedFields(dest interface{}) bool {
t := reflect.TypeOf(dest)
if t == nil || t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
return false
}
for i := 0; i < t.Elem().NumField(); i++ {
if t.Elem().Field(i).PkgPath == "" { return true }
}
return false
} Try / catch
if err := rows.StructScan(&s); err != nil {
if strings.Contains(err.Error(), "no exported fields") {
return fmt.Errorf("model %T must export scanned fields", s)
}
return err
} Prevention
- Export fields on any struct used as a scan destination.
- Use separate DTOs with exported fields for DB rows.
- Lint for lowercase-only model structs.
When it happens
Trigger: Calling StructScan/Get/Select with a struct destination where every field is unexported (lowercase) or the struct is empty.
Common situations: Defining model structs with all-lowercase fields; internal/config structs reused as scan destinations; generating structs with private fields.
Related errors
- missing destination name %s in %T
- must pass a pointer, not a value, to StructScan destination
- expected %s but got %s
- structscan expects a struct dest but the provided struct typ
- empty slice passed to 'in' query
AI-assisted analysis of jmoiron/sqlx@41dac167fd (2026-09-03).
Data as JSON: /api/errors/a9797e1a4edc8775.
Report an issue: GitHub.