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

  1. Inspect the backup JSON at the failing field and make it a JSON array (e.g. change {"k":v} or "x" to [...]).
  2. 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.
  3. Regenerate the backup from a matching version of the source system instead of editing it manually.
  4. 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

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


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)