github/copilot-sdk · error · IllegalStateException

Failed to serialize tool result to JSON

Error message

Failed to serialize tool result to JSON

What it means

When a tool handler returns a result, ToolDefinition.formatResult serializes it to JSON via Jackson (unless it is already a ToolResultObject). If Jackson cannot serialize the returned object (no accessible getters, unsupported types, self-referencing structures, failing custom serializers), an IllegalStateException wrapping the JsonProcessingException is thrown with this message.

Solutions

  1. Read the wrapped cause (ex.getCause()) to identify the failing property or type.
  2. Return a ToolResultObject (or a serializable DTO/String/Map) from the handler instead of the raw domain object.
  3. Fix the returned type: add getters, break cyclic references with @JsonManagedReference/@JsonBackReference or @JsonIgnore.
  4. Register needed Jackson modules on the SDK-configured ObjectMapper (or return pre-serialized data) so types like LocalDate serialize.

Example fix

// before
public Object getReport(String id) { return reportRepo.find(id); } // cyclic entity

// after
public Object getReport(String id) {
    Report r = reportRepo.find(id);
    return Map.of("id", r.getId(), "title", r.getTitle()); // serializable DTO
}
Defensive patterns

Strategy: validation

Validate before calling

new ObjectMapper().writeValueAsString(result); // dry-run serialize in tests

Type guard

boolean serializable(Object o) {
  try { mapper.writeValueAsString(o); return true; } catch (JsonProcessingException e) { return false; }
}

Try / catch

try { return tool.call(invocation); } catch (IllegalStateException e) {
  log.error("tool result serialization failed", e.getCause());
  return Map.of("error", "result could not be serialized");
}

Prevention

When it happens

Trigger: A tool handler registered via from/fromAsync/fromWithToolInvocation/fromAsyncWithToolInvocation returns an object Jackson cannot write: a class with no getters, an infinite recursion (parent/child cyclic reference), or a type with a broken custom JsonSerializer.

Common situations: Returning domain entities with bidirectional relationships causing infinite recursion (JsonMappingException); returning objects not designed for serialization (InputStream, lambdas); Jackson version/config mismatches after upgrading; returning records/POJOs with incompatible module (e.g. missing jackson-datatype-jsr310 for LocalDate).

Understand the failure class

Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/54b935945b467b51. Report an issue: GitHub.

Appendix: source

Thrown at java/sdk/src/main/java/com/github/copilot/rpc/ToolDefinition.java:898

     * <li>{@code null} — mapped to {@code "Success"} (covers handlers that return
     * null to indicate a successful no-value result)</li>
     * <li>any other value — JSON-serialized via {@link ObjectMapper}</li>
     * </ul>
     */
    private static Object formatResult(Object result, ObjectMapper mapper) {
        if (result == null) {
            return "Success";
        }
        if (result instanceof String) {
            return result;
        }
        if (result instanceof ToolResultObject) {
            return result;
        }
        try {
            return mapper.writeValueAsString(result);
        } catch (com.fasterxml.jackson.core.JsonProcessingException ex) {
            throw new IllegalStateException("Failed to serialize tool result to JSON", ex);
        }
    }

    // ------------------------------------------------------------------
    // Validation helpers
    // ------------------------------------------------------------------

    private static void requireNonBlankToolName(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Tool name must not be null or blank");
        }
    }

    private static void requireNonBlankDescription(String description) {
        if (description == null || description.isBlank()) {
            throw new IllegalArgumentException("Tool description must not be null or blank");
        }
    }

View on GitHub (pinned to cd8cf15dc3)