plandex-ai/plandex · error

Error marshalling plans:

Error message

Error marshalling plans: 

What it means

In ListPlansHandler's writePlans closure (plans_crud.go:311), json.Marshal fails to serialize the []*shared.Plan slice to JSON. Since Plan is a plain data struct, this is rare — it happens only if the struct (or something it references) contains an unserializable value such as a channel, func, or a cyclic reference, or a custom MarshalJSON that errors.

Source

Thrown at app/server/handlers/plans_crud.go:311

func ListPlansHandler(w http.ResponseWriter, r *http.Request) {
	log.Println("Received request for ListPlans")

	auth := Authenticate(w, r, true)
	if auth == nil {
		return
	}

	projectIds := r.URL.Query()["projectId"]

	log.Println("projectIds: ", projectIds)

	var apiPlans []*shared.Plan

	writePlans := func() {
		jsonBytes, err := json.Marshal(apiPlans)
		if err != nil {
			log.Printf("Error marshalling plans: %v\n", err)
			http.Error(w, "Error marshalling plans: "+err.Error(), http.StatusInternalServerError)
			return
		}

		w.Write(jsonBytes)
	}

	if len(projectIds) == 0 {
		writePlans()
		return
	}

	authorizedProjectIds := []string{}
	for _, projectId := range projectIds {
		if authorizeProjectOptional(w, projectId, auth, false) {
			authorizedProjectIds = append(authorizedProjectIds, projectId)
		}
	}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Inspect the logged marshal error — it names the unsupported type/field; add a json:"-" tag or remove the field from the API struct.
  2. Ensure ToApi() returns a dedicated API DTO containing only JSON-safe primitives.
  3. Implement MarshalJSON on the offending type if it must be represented differently.
  4. Add a unit test marshalling an example Plan to catch regressions early.

Example fix

// before
type Plan struct {
    Id     string
    DoneCh chan struct{} // unsupported
}
// after
type Plan struct {
    Id     string `json:"id"`
    DoneCh chan struct{} `json:"-"`
}
Defensive patterns

Strategy: type-guard

Validate before calling

// in tests: ensure API plans are always JSON-safe
if err := json.Marshal(plan.ToApi()); err != nil {
    t.Fatalf("Plan is not JSON-serializable: %v", err)
}

Type guard

func isJSONSafe(v any) bool {
    switch v.(type) {
    case chan struct{}, func(), map[string]chan int, complex128:
        return false
    }
    return json.NewValidator != nil // fall back to json.Valid on marshalled bytes
}
// simpler: marshal and check
func jsonSafe(b []byte) bool { return json.Valid(b) }

Try / catch

jsonBytes, err := json.Marshal(apiPlans)
if err != nil {
    log.Printf("Error marshalling plans: %v", err)
    http.Error(w, "marshal failure", http.StatusInternalServerError)
    return
}

Prevention

When it happens

Trigger: json.Marshal(apiPlans) returns an error because a field added to shared.Plan is of an unsupported type (chan, func, complex), Plan.ToApi() populated such a field, or a custom MarshalJSON method on Plan/contained types returns an error.

Common situations: A developer adds a non-serializable field (e.g. a done channel or callback) to the shared Plan struct without a json tag or custom marshaller; introducing a reference cycle via pointers.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/674f62e662875904. Report an issue: GitHub.