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

  1. Export the struct fields you want scanned (capitalize them) and add db tags.
  2. Use a different struct with exported fields as the scan destination.
  3. 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

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


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