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

  1. Inspect the backup JSON at the reported field and change the value to a JSON object ({...}) matching the struct shape.
  2. Verify you pass the correct top-level object to Unmarshal - if the backup root is an array, unmarshal into a slice target instead.
  3. Make the Go field a slice ([]T) if the data is legitimately an array.
  4. 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

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


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)