semaphoreui/semaphore · error

expected slice for field

Error message

expected slice for field %s but got %T

What it means

When a struct field is a slice, the source value must be a Go slice/array or a string parseable as a slice (JSON array, or single string for []string fields). Any other kind (int, bool, float, nil-typed value, etc.) returns this error. It reports that a non-slice value was supplied for a slice field.

Solutions

  1. Wrap the scalar in a slice in the source map: []any{value} or []string{value}.
  2. Check whether the struct field type is correct — if the value is genuinely scalar, change the field or the key.
  3. Validate the map values against the struct shape (reflect over structType fields) before assignment.
  4. If the value comes from env/file config, pre-parse it into the proper slice type.

Example fix

// before
m := map[string]any{"hosts": "web-1"} // field is []string? no — scalar in map for slice field of non-string elem
// after
m := map[string]any{"hosts": []any{"web-1"}}
Defensive patterns

Strategy: validation

Validate before calling

func expectSlice(m map[string]any, key string) error {
  v, ok := m[key]
  if !ok { return nil }
  switch v.(type) {
  case []any, []string, []int, string:
    return nil
  default:
    return fmt.Errorf("%s must be a slice or JSON array string, got %T", key, v)
  }
}

Type guard

func isSliceLike(v any) bool {
  switch v.(type) {
  case []any, []string, []int, []map[string]any:
    return true
  }
  rv := reflect.ValueOf(v)
  return rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array
}

Try / catch

if err := util.AssignMapToStruct(m, &cfg); err != nil {
  if strings.Contains(err.Error(), "expected slice for field") {
    log.Fatalf("config value must be a list: %v", err)
  }
}

Prevention

When it happens

Trigger: Calling AssignMapToStruct with m["field"] = 42 or true while the target struct field is a slice type (e.g. []string, []int).

Common situations: Config schema drift: a field changed from scalar to slice (or vice versa) and old config maps still hold a scalar; hand-built maps where the wrong key was set; merged config where a value was overwritten by a scalar default.

Related errors


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/fa9b82ecc010c77e. Report an issue: GitHub.

Appendix: source

Thrown at util/config.go:1198

					var sourceSlice reflect.Value
					if val.Kind() == reflect.Slice || val.Kind() == reflect.Array {
						sourceSlice = val
					} else if val.Kind() == reflect.String {
						// Try to parse JSON array from string
						str := val.String()
						// First, try to unmarshal into []any
						var anyArr []any
						if err := json.Unmarshal([]byte(str), &anyArr); err == nil {
							sourceSlice = reflect.ValueOf(anyArr)
						} else if fieldElemType.Kind() == reflect.String {
							// Fallback: treat as single element string
							sourceSlice = reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf("")), 1, 1)
							sourceSlice.Index(0).SetString(str)
						} else {
							return fmt.Errorf("expected slice or json array string for field %s but got %T", field.Name, value)
						}
					} else {
						return fmt.Errorf("expected slice for field %s but got %T", field.Name, value)
					}

					// Build destination slice
					newSlice := reflect.MakeSlice(fieldValue.Type(), 0, sourceSlice.Len())
					for i := 0; i < sourceSlice.Len(); i++ {
						srcElemVal := sourceSlice.Index(i)
						// When source is []any, elements come as interface{}, unwrap reflect.Value
						if srcElemVal.Kind() == reflect.Interface && !srcElemVal.IsNil() {
							srcElemVal = reflect.ValueOf(srcElemVal.Interface())
						}

						var dstElem reflect.Value
						// Prepare destination element
						if fieldElemType.Kind() == reflect.Struct {
							dstElem = reflect.New(fieldElemType).Elem()
							if srcElemVal.Kind() == reflect.Map {
								// Expect map[string]any
								mIface, ok := srcElemVal.Interface().(map[string]any)

View on GitHub (pinned to 1774ccb71a)