Repository navigation
Use Optional return types instead of nullable boxed primitives in public API #187
Description
Summary
The public API of this SDK uses nullable boxed types (Boolean, Integer, Double) on mutable config/builder classes to represent optional values. This is a C# idiom — not a Java one. Java has had Optional<T>, OptionalInt, and OptionalDouble since Java 8 (2014), and returning Optional from getters is the established idiomatic way to express "this value may be absent."
Today the SDK has ~30 public getters on mutable classes returning nullable boxed types and zero uses of Optional anywhere in non-generated source code. This is a clear signal that the nullable-return convention was carried over from the C# reference implementation rather than adapted to Java idioms during the port.
Scope
What to change
Only mutable config/builder classes (regular classes with hand-written getters/setters). These classes all have tri-state semantics where null means "use the server/runtime default" — a contract that is invisible in the current type signatures but becomes explicit with Optional.
What NOT to change
- Record types — Record accessor methods are auto-generated from components.
Optionalas a record component is unidiomatic and clashes with Jackson deserialization. Records likePingResponse,ToolDefinition,PreToolUseHookOutput,PostToolUseHookOutput,SessionEndHookOutput, andUserPromptSubmittedHookOutputare explicitly out of scope. - Generated code under
src/generated/java/— do not modify. - Internal fields — Fields remain as nullable boxed types for Jackson serialization (
@JsonInclude(NON_NULL)). Only the getter return types and setter parameter types change.
Specification
For each affected getter/setter pair, apply this transformation:
For Boolean fields
Before:
private Boolean enableSessionTelemetry;
public Boolean getEnableSessionTelemetry() {
return enableSessionTelemetry;
}
public SessionConfig setEnableSessionTelemetry(Boolean enableSessionTelemetry) {
this.enableSessionTelemetry = enableSessionTelemetry;
return this;
}After:
private Boolean enableSessionTelemetry;
public Optional<Boolean> getEnableSessionTelemetry() {
return Optional.ofNullable(enableSessionTelemetry);
}
public SessionConfig setEnableSessionTelemetry(boolean enableSessionTelemetry) {
this.enableSessionTelemetry = enableSessionTelemetry;
return this;
}
public SessionConfig clearEnableSessionTelemetry() {
this.enableSessionTelemetry = null;
return this;
}For Integer fields
Before:
private Integer sessionIdleTimeoutSeconds;
public Integer getSessionIdleTimeoutSeconds() {
return sessionIdleTimeoutSeconds;
}
public CopilotClientOptions setSessionIdleTimeoutSeconds(Integer sessionIdleTimeoutSeconds) {
this.sessionIdleTimeoutSeconds = sessionIdleTimeoutSeconds;
return this;
}After:
private Integer sessionIdleTimeoutSeconds;
public OptionalInt getSessionIdleTimeoutSeconds() {
return sessionIdleTimeoutSeconds == null ? OptionalInt.empty() : OptionalInt.of(sessionIdleTimeoutSeconds);
}
public CopilotClientOptions setSessionIdleTimeoutSeconds(int sessionIdleTimeoutSeconds) {
this.sessionIdleTimeoutSeconds = sessionIdleTimeoutSeconds;
return this;
}
public CopilotClientOptions clearSessionIdleTimeoutSeconds() {
this.sessionIdleTimeoutSeconds = null;
return this;
}For Double fields
Same pattern, using OptionalDouble and primitive double setter.
Affected classes and fields
Public API config classes (user-facing)
CopilotClientOptions.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getSessionIdleTimeoutSeconds() |
Integer |
OptionalInt |
int |
getUseLoggedInUser() |
Boolean |
Optional<Boolean> |
boolean |
SessionConfig.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getEnableSessionTelemetry() |
Boolean |
Optional<Boolean> |
boolean |
getEnableConfigDiscovery() |
Boolean |
Optional<Boolean> |
boolean |
getIncludeSubAgentStreamingEvents() |
Boolean |
Optional<Boolean> |
boolean |
ResumeSessionConfig.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getEnableSessionTelemetry() |
Boolean |
Optional<Boolean> |
boolean |
getEnableConfigDiscovery() |
Boolean |
Optional<Boolean> |
boolean |
getIncludeSubAgentStreamingEvents() |
Boolean |
Optional<Boolean> |
boolean |
InfiniteSessionConfig.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getEnabled() |
Boolean |
Optional<Boolean> |
boolean |
getBackgroundCompactionThreshold() |
Double |
OptionalDouble |
double |
getBufferExhaustionThreshold() |
Double |
OptionalDouble |
double |
InputOptions.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getMinLength() |
Integer |
OptionalInt |
int |
getMaxLength() |
Integer |
OptionalInt |
int |
ModelCapabilitiesOverride.Supports (inner class)
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getVision() |
Boolean |
Optional<Boolean> |
boolean |
getReasoningEffort() |
Boolean |
Optional<Boolean> |
boolean |
ModelCapabilitiesOverride.Limits (inner class)
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getMaxPromptTokens() |
Integer |
OptionalInt |
int |
getMaxOutputTokens() |
Integer |
OptionalInt |
int |
getMaxContextWindowTokens() |
Integer |
OptionalInt |
int |
ProviderConfig.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getMaxPromptTokens() |
Integer |
OptionalInt |
int |
getMaxOutputTokens() |
Integer |
OptionalInt |
int |
TelemetryConfig.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getCaptureContent() |
Boolean |
Optional<Boolean> |
boolean |
SessionUiCapabilities.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getElicitation() |
Boolean |
Optional<Boolean> |
boolean |
CustomAgentConfig.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getInfer() |
Boolean |
Optional<Boolean> |
boolean |
UserInputRequest.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getAllowFreeform() |
Boolean |
Optional<Boolean> |
boolean |
Internal wire DTOs (not directly user-facing, but same pattern applies)
CreateSessionRequest.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getEnableSessionTelemetry() |
Boolean |
Optional<Boolean> |
boolean |
getRequestPermission() |
Boolean |
Optional<Boolean> |
boolean |
getRequestUserInput() |
Boolean |
Optional<Boolean> |
boolean |
getHooks() |
Boolean |
Optional<Boolean> |
boolean |
getStreaming() |
Boolean |
Optional<Boolean> |
boolean |
getEnableConfigDiscovery() |
Boolean |
Optional<Boolean> |
boolean |
getIncludeSubAgentStreamingEvents() |
Boolean |
Optional<Boolean> |
boolean |
getRequestElicitation() |
Boolean |
Optional<Boolean> |
boolean |
ResumeSessionRequest.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getEnableSessionTelemetry() |
Boolean |
Optional<Boolean> |
boolean |
getRequestPermission() |
Boolean |
Optional<Boolean> |
boolean |
getRequestUserInput() |
Boolean |
Optional<Boolean> |
boolean |
getHooks() |
Boolean |
Optional<Boolean> |
boolean |
getEnableConfigDiscovery() |
Boolean |
Optional<Boolean> |
boolean |
getDisableResume() |
Boolean |
Optional<Boolean> |
boolean |
getStreaming() |
Boolean |
Optional<Boolean> |
boolean |
getIncludeSubAgentStreamingEvents() |
Boolean |
Optional<Boolean> |
boolean |
getRequestElicitation() |
Boolean |
Optional<Boolean> |
boolean |
McpServerConfig.java (abstract base)
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getTimeout() |
Integer |
OptionalInt |
int |
ModelLimits.java
| Getter | Current return | New return | Setter param |
|---|---|---|---|
getMaxPromptTokens() |
Integer |
OptionalInt |
int |
Caller updates required
All internal callers that null-check these getters must be updated to use Optional APIs. Common patterns:
// BEFORE (e.g., CliServerManager.java)
if (options.getSessionIdleTimeoutSeconds() != null) {
args.add("--session-idle-timeout");
args.add(String.valueOf(options.getSessionIdleTimeoutSeconds()));
}
// AFTER
options.getSessionIdleTimeoutSeconds().ifPresent(timeout -> {
args.add("--session-idle-timeout");
args.add(String.valueOf(timeout));
});// BEFORE (e.g., CliServerManager.java)
if (options.getUseLoggedInUser() != null && options.getUseLoggedInUser()) {
args.add("--use-logged-in-user");
}
// AFTER
options.getUseLoggedInUser().filter(v -> v).ifPresent(v ->
args.add("--use-logged-in-user")
);// BEFORE (e.g., SessionRequestBuilder.java)
Boolean value = config.getEnableSessionTelemetry();
if (value != null) {
request.setEnableSessionTelemetry(value);
}
// AFTER
config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry);Jackson serialization
No changes needed to Jackson configuration. The internal fields remain as nullable boxed types (Boolean, Integer, Double) with @JsonInclude(NON_NULL). Jackson reads/writes the fields directly — the Optional wrapper exists only on the getter return type, not on the field or in the serialized JSON.
Testing
- All existing tests must continue to pass after the changes.
- The
clearXxx()methods should be exercised in tests to verify they reset the field to null (and that Jackson then omits the field from serialized output). - Callers updated from null-check patterns to
Optionalpatterns should produce identical behavior.
Why this matters
| Dimension | Current (nullable boxed) | Proposed (Optional) |
|---|---|---|
| Nullability visible in type signature? | No | Yes |
| Forgotten null-check consequence | Compiles, NPEs at runtime | Does not compile |
| Idiomatic Java? | No (C# convention) | Yes (standard since Java 8) |
Optional usage in SDK today |
0 instances | — |
| Records affected? | — | None (explicitly excluded) |
This is a breaking API change for getter return types and should be planned accordingly.
Activity
edburns commentedon May 14, 2026
commit
8e0eeebcompletely rolled back all the Optional usage from issue #187.** Here's what happened:Timeline in PR #189
Commits 1-7 (6e8b100 → acd5c4a): Correctly implemented the Optional pattern per Use Optional return types instead of nullable boxed primitives in public API #187 —
Optional<Boolean>,OptionalInt,OptionalDoublegetters, primitive setters,clearXxx()methods,@JsonIgnoreon Optional-returning getters.Commit 8 (8e0eeeb): Message says "refactor: revert Optional usage, use plain nullable fields with @JsonInclude(NON_NULL)". This commit undoes all of the work from commits 1-7:
Optional<Boolean>→ back toBooleanOptionalInt→ back toIntegerOptionalDouble→ back toDoubleclearXxx()methods deleted@JsonIgnoreannotations removedOptionalApiAndJacksonTest.java(635 lines) deleted entirelyboolean,int,double) back to boxed (Boolean,Integer,Double)You approved and merged commit 8, so the final state on
mainhas none of the Optional usage that Use Optional return types instead of nullable boxed primitives in public API #187 specified.What was preserved from the PR
The only lasting changes from the PR are:
@JsonInclude(JsonInclude.Include.NON_NULL)added to 4 classes that were missing it (InputOptions,TelemetryConfig,SessionUiCapabilities,UserInputRequest)JsonIncludeNonNullTest.java(new, 159 lines)Boolean.TRUE.equals(...)instead of!= nullchecks)Bottom line
It appears Copilot's session
1ee0ec69was instructed (or interpreted an instruction) to revert the Optional pattern entirely, producing commit 8e0eeeb. You then approved and merged it, effectively closing #187 without its intended changes actually landing. If the Optional pattern is still desired, you'd need to re-do the work from commits 1-7.Completed: Check PR #189 for Optional usage rollback (1/3)
Restore Optional public API from #187 and add missing @JsonInclude an…
edburns commentedon May 15, 2026
Now that #200 has been merged, I am closing this.