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
- Read the wrapped cause (ex.getCause()) to identify the failing property or type.
- Return a ToolResultObject (or a serializable DTO/String/Map) from the handler instead of the raw domain object.
- Fix the returned type: add getters, break cyclic references with @JsonManagedReference/@JsonBackReference or @JsonIgnore.
- 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
- Return simple DTOs/Maps/Strings from tool handlers, not domain entities.
- Annotate cyclic relationships with @JsonIgnore or DTO-mapping.
- Unit-test every handler by serializing its output in CI.
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
- Failed to serialize FFI JSON parameter.
- Failed to coerce parameter ' + param.name() + ' to type +…
- Failed to deserialize arguments to + type.getName()
- Unknown WorkspaceDiffFileChangeType value
- Unknown WorkspaceDiffMode value
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)