semaphoreui/semaphore · error
expected object for struct, got %T
Error message
expected object for struct, got %T
What it means
unmarshalValueWithBackupTags (services/project/backup_marshal.go:198) handles struct destinations by requiring the corresponding JSON data to be a map[string]any (a JSON object). If the data is any other type - string, number, array, bool, or null - it returns "expected object for struct, got %T" before recursing into unmarshalStructWithBackupTags.
Solutions
- Inspect the backup JSON at the reported field and change the value to a JSON object ({...}) matching the struct shape.
- Verify you pass the correct top-level object to Unmarshal - if the backup root is an array, unmarshal into a slice target instead.
- Make the Go field a slice ([]T) if the data is legitimately an array.
- Re-export the backup with the current tool version so the JSON shapes match the struct definitions.
Example fix
// before (backup.json)
"inventory": ["inv1", "inv2"] // array, target is struct
// after
"inventory": {"name": "inv1", "project_id": 1} Defensive patterns
Strategy: type-guard
Validate before calling
var raw any
json.Unmarshal(backupData, &raw)
if _, ok := raw.(map[string]any); !ok {
return errors.New("backup root must be a JSON object")
} Type guard
func isJSONObject(v any) bool { _, ok := v.(map[string]any); return ok } Try / catch
if err := project.Unmarshal(data, &target); err != nil {
if strings.Contains(err.Error(), "expected object for struct") {
return fmt.Errorf("backup JSON shape does not match struct: %w", err)
}
return err
} Prevention
- Ensure struct fields map to JSON objects, not arrays or scalars.
- Unmarshal JSON arrays into slice targets, not structs.
- Validate backup JSON structure against the entity schema before restoring.
When it happens
Trigger: During backup restore, a struct-kind field is matched with backup JSON whose value is not an object - e.g. the field maps to a JSON string/array/number, or the whole payload passed to Unmarshal is not a JSON object while the target is a struct.
Common situations: Backup file where a nested entity was serialized as an array or scalar instead of an object; passing the wrong data element to Unmarshal (e.g. a JSON array of objects into a struct target); hand-edited or corrupted backup JSON; version drift changing a field from object to array.
Related errors
- expected bool for field, got %T
- expected string for field, got %T
- expected number for field, got %T
- must be valid JSON
- key can not be empty
AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07).
Data as JSON: /api/errors/5808cdb701f0483f.
Report an issue: GitHub.
Appendix: source
Thrown at services/project/backup_marshal.go:198
}
}
func unmarshalValueWithBackupTags(data any, v reflect.Value) error {
// Handle pointers
if v.Kind() == reflect.Ptr {
// Initialize pointer if it's nil
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
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)View on GitHub (pinned to 1774ccb71a)