semaphoreui/semaphore · error
expected array for slice, got %T
Error message
expected array for slice, got %T
What it means
unmarshalValueWithBackupTags decodes backup JSON into Go values via reflection. When the destination is a slice (reflect.Slice) but the decoded JSON value is not []any (e.g. an object, string, or number), it returns this error. It signals a shape mismatch between the backup file and the target struct field tagged with backup tags.
Solutions
- Inspect the backup JSON at the failing field and make it a JSON array (e.g. change {"k":v} or "x" to [...]).
- Ensure the Go struct field type matches the JSON shape; if the field is now an object, change the struct field to a map or struct.
- Regenerate the backup from a matching version of the source system instead of editing it manually.
- Pre-validate the JSON shape with encoding/json unmarshalling into the target struct before calling Unmarshal to get precise paths.
Example fix
// before (backup.json)
"users": {"name": "admin"}
// after
"users": [{"name": "admin"}] Defensive patterns
Strategy: validation
Validate before calling
var raw map[string]json.RawMessage
if err := json.Unmarshal(backupJSON, &raw); err != nil { return err }
for field, v := range raw {
var arr []any
if needsSlice(field) && json.Unmarshal(v, &arr) != nil {
return fmt.Errorf("field %q must be a JSON array, got %s", field, string(v))
}
} Type guard
func isJSONArray(v any) bool { _, ok := v.([]any); return ok } Try / catch
if err := project.Unmarshal(data, &dst); err != nil {
if strings.Contains(err.Error(), "expected array for slice") {
// log offending field, fix backup JSON or regenerate backup
}
return err
} Prevention
- Never hand-edit backup JSON; always regenerate from the source system.
- Keep backup producer and consumer on matching schema versions.
- Run a JSON-schema validation pass over backup files before unmarshalling.
When it happens
Trigger: Calling project.Unmarshal (or unmarshalValueWithBackupTags directly) on backup JSON where a field whose Go type is a slice was serialized as a non-array JSON value (object, string, number, bool, or null path falling through to this branch).
Common situations: Hand-edited or externally generated backup files where an array field was replaced by an object or scalar; schema drift between Semaphore versions where a field changed from slice to map; partial JSON merges that swapped the value type.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- expected object for map[string]interface
- expected object for map, got %T
- cannot set value
- unsupported kind
- expected bool for field, got %T
AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07).
Data as JSON: /api/errors/7de7f7916c28906b.
Report an issue: GitHub.
Appendix: source
Thrown at services/project/backup_marshal.go:207
}
return unmarshalValueWithBackupTags(data, v.Elem())
}
// Handle structs
if v.Kind() == reflect.Struct {
// Data should be a map
m, ok := data.(map[string]any)
if !ok {
return fmt.Errorf("expected object for struct, got %T", data)
}
return unmarshalStructWithBackupTags(m, v)
}
// Handle slices and arrays
if v.Kind() == reflect.Slice {
dataSlice, ok := data.([]any)
if !ok {
return fmt.Errorf("expected array for slice, got %T", data)
}
slice := reflect.MakeSlice(v.Type(), len(dataSlice), len(dataSlice))
for i := 0; i < len(dataSlice); i++ {
elem := slice.Index(i)
if err := unmarshalValueWithBackupTags(dataSlice[i], elem); err != nil {
return err
}
}
v.Set(slice)
return nil
}
if v.Type() == reflect.TypeOf(db.MapStringAnyField{}) {
// Data should be a map
m, ok := data.(map[string]any)
if !ok {
return fmt.Errorf("expected object for map[string]interface{}, got %T", data)
}View on GitHub (pinned to 1774ccb71a)