Add classical code generation workflow for Java SDK#70
Conversation
- Add scripts/codegen/java.ts - Java code generator from session-events and api schemas - Add scripts/codegen/package.json with @github/copilot dependency - Add scripts/codegen/package-lock.json - Generate src/generated/java/com/github/copilot/sdk/generated/ - 76 session event files - Generate src/generated/java/com/github/copilot/sdk/generated/rpc/ - 111 RPC type files - Update pom.xml: add build-helper-maven-plugin, exclude generated from Spotless - Update config/checkstyle/checkstyle.xml: exclude generated packages - Update .gitattributes: mark src/generated/java/** as generated - Add .github/workflows/codegen-check.yml - CI check workflow - Add .github/workflows/update-copilot-dependency.yml - update workflow - Update docs/WORKFLOWS.md with new workflow documentation - Update .gitignore to exclude scripts/codegen/node_modules/" Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/aee4deda-40d4-4ecd-a831-2af9cb9461e7 Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
| - name: Update @github/copilot in scripts/codegen | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| working-directory: ./scripts/codegen |
There was a problem hiding this comment.
@copilot add a comment explaining the necessity of the unusual step of running npm install before npm ci
There was a problem hiding this comment.
Added a comment in 6e3c50c explaining the two-step approach: npm install updates package.json and package-lock.json to pin the new version, then npm ci does a clean reproducible install from the updated lock file. Running npm ci alone would fail because it cannot change a pinned version, and npm install alone leaves behind stale packages.
Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/153bf73a-7c73-4f85-99be-ea7df4e5a2b6 Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
edburns
left a comment
There was a problem hiding this comment.
Epic #69: Improve Quality of Java Code Generator
Context
The file scripts/codegen/java.ts is a TypeScript code generator that reads JSON Schema files from the @github/copilot npm dependency and emits Java source files under src/generated/java/. Its purpose is to replace hand-written Java classes in src/main/java/com/github/copilot/sdk/events/ and src/main/java/com/github/copilot/sdk/json/ with auto-generated equivalents.
The current generator produces Java code that is significantly less idiomatic than the hand-written classes it is meant to replace. This prompt describes the required improvements.
Reference files
Before making any changes, read and understand these files:
scripts/codegen/java.ts— the generator to modifysrc/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java— the hand-written sealed event hierarchy (the gold standard)src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java— example of record-based data payload inside a final event classsrc/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java— minimal event with empty record datasrc/main/java/com/github/copilot/sdk/events/UnknownSessionEvent.java— forward-compatibility fallbacksrc/main/java/com/github/copilot/sdk/events/SessionEventParser.java— manual type-map-based deserializationsrc/main/java/com/github/copilot/sdk/json/ToolDefinition.java— top-level record DTOsrc/main/java/com/github/copilot/sdk/json/Attachment.java— top-level record DTOsrc/main/java/com/github/copilot/sdk/json/SessionConfig.java— mutable config class with fluent setterssrc/main/java/com/github/copilot/sdk/json/ModelInfo.java— mutable bean with fluent setterssrc/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java— extensible enum pattern (string-backed value type with static constants and@JsonCreator)src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java— classic enum with string backing value
Also read .github/copilot-instructions.md for project conventions.
Requirements
1. Use sealed class for the session event hierarchy
The generated SessionEvent.java base class must be declared as:
public abstract sealed class SessionEvent permits
AssistantMessageEvent, SessionIdleEvent, ...
UnknownSessionEvent {The generator must collect all variant class names and emit them in the permits clause. UnknownSessionEvent must always be the last entry.
The hand-written AbstractSessionEvent.java is the reference for this pattern.
2. Use record types for event data payloads
Every event variant's inner Data class must be generated as a Java record, not a mutable bean. For example, ToolExecutionStartEvent should contain:
@JsonIgnoreProperties(ignoreUnknown = true)
public record ToolExecutionStartData(
@JsonProperty("toolCallId") String toolCallId,
@JsonProperty("toolName") String toolName,
@JsonProperty("arguments") Object arguments
) {}Not:
public static class ToolExecutionStartData {
private String toolCallId;
public String getToolCallId() { return toolCallId; }
public void setToolCallId(String toolCallId) { this.toolCallId = toolCallId; }
// ...
}Records are appropriate here because event data payloads are immutable value types deserialized from JSON — they are never constructed by SDK users or mutated after creation.
3. Use record types for RPC params and result classes
The RPC ...Params and ...Result classes generated from api.schema.json should also be records. These are request/response DTOs that are deserialized from JSON and not mutated.
4. Distinguish integer from number in type mapping
The current generator maps both "integer" and "number" to Double. This is incorrect.
- JSON Schema
"integer"→ JavaLong(orIntegerif the schema constrains the range, but default toLong) - JSON Schema
"number"→ JavaDouble
Using floating point for integer values is dangerous and unacceptable. Fix schemaTypeToJava() to distinguish these cases.
5. Use primitive types for required fields, boxed types for optional
The required parameter is already computed in schemaTypeToJava() but not used to inform the Java type. Change this:
- Required
booleanfield →boolean(primitive) - Optional
booleanfield →Boolean(boxed, nullable) - Required
integerfield →long(primitive) - Optional
integerfield →Long(boxed, nullable) - Required
numberfield →double(primitive) - Optional
numberfield →Double(boxed, nullable)
Note: this applies to mutable bean classes only. Records always use boxed types because record components are nullable by default and there is no way to distinguish "field absent from JSON" vs "field present with default value" with primitives.
6. Make event subclasses final
Every generated event variant class must be declared public final class ...Event extends SessionEvent. This cooperates with the sealed hierarchy and prevents accidental subclassing.
7. Reduce fallback to Object
The current generator falls back to Object for:
- Multi-type
anyOfunions with more than one non-null branch - Unrecognized schema patterns
- Untyped schemas
For each fallback site, add a console.warn() logging the schema path and context so that silent type erasure is visible during generation. Do not silently swallow schema information.
Additionally, for anyOf unions of exactly two concrete types where one is string, prefer String over Object — strings are the most common wire-level fallback.
8. Remove dead code
The function generateDataClass (around line 215) appears to be unused — it is not called from generateSessionEvents, generateRpcTypes, or main. Verify this claim:
- Search for all call sites of
generateDataClassinjava.tsand any other files inscripts/codegen/. - If truly dead, remove it entirely.
- If it is used, fix its return type (currently returns an object but is typed as returning
string, cast viaas any).
9. Improve enum generation
Current enums use @JsonValue on a getValue() method but lack @JsonCreator for deserialization. Add a static @JsonCreator factory method to each generated enum:
@JsonCreator
public static MyEnum fromValue(String value) {
for (MyEnum v : values()) {
if (v.value.equals(value)) return v;
}
throw new IllegalArgumentException("Unknown value: " + value);
}10. Generate Javadoc
- Each generated class must have a Javadoc comment. If the schema provides a
description, use it. Otherwise, generate a sensible default (e.g.,/** The {@code session.idle} session event. */). - Each record component or field with a schema
descriptionmust have a Javadoc comment. - Add
@sincetags based on the current SDK version.
11. Add @JsonInclude(NON_NULL) consistently
The hand-written records use @JsonInclude(JsonInclude.Include.NON_NULL) at the class level. The generated code should do the same on all generated types.
Constraints
- Do not modify any hand-written files in
src/main/java/. This task is only about improving the generator. - Do not change the output directory (
src/generated/java/com/github/copilot/sdk/generated/). - Do not change the package name (
com.github.copilot.sdk.generated). - After making changes, run the generator and verify the output compiles:
cd scripts/codegen && npm ci && npx tsx java.ts && cd ../.. && mvn compile -pl . - Run
mvn spotless:applybefore committing. - The generated code must be compatible with Java 17 (the project baseline).
- Jackson 2.x must be able to deserialize the generated records and sealed class hierarchy correctly. Test this by running
mvn verify.
Verification checklist
After completing the changes, verify:
-
SessionEvent.javaissealedwith apermitsclause listing all event classes plusUnknownSessionEvent - All event
...Datainner classes arerecordtypes - All RPC
...Paramsand...Resultclasses arerecordtypes -
"integer"schema type maps toLong,"number"maps toDouble - Required primitive fields use unboxed types in mutable classes
- All event subclasses are
public final class -
generateDataClassis removed (or fixed if actually used) - Enums have
@JsonCreatorfactory methods - No silent
Objectfallbacks — each one logs a warning during generation -
mvn verifypasses with all existing tests -
mvn spotless:applyproduces no changes after a clean generation
…ger, @JsonCreator Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/c76bbc9a-e73e-40cc-8dd4-7ecd223c16cd Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
All eleven improvements have been implemented in commit 75e5dba:
|
Implements the classical code-gen pipeline for the Java SDK, analogous to the dotnet/nodejs/python generators in
copilot-sdk/scripts/codegen/. Consumessession-events.schema.jsonandapi.schema.jsonfrom the@github/copilotnpm package to generate typed Java source files.Before the change?
After the change?
Codegen script (
scripts/codegen/java.ts)session-events.schema.json→ generatescom.github.copilot.sdk.generated(76 files):SessionEventbase with full@JsonTypeInfo/@JsonSubTypespolymorphism, one typed event class per variant,UnknownSessionEventfallbackapi.schema.json→ generatescom.github.copilot.sdk.generated.rpc(111 files): params/result DTOs for all 61 RPC methods// AUTO-GENERATED FILE - DO NOT EDIT, schema-source comment, and@javax.annotation.processing.Generated("copilot-sdk-codegen")Maven (
pom.xml)build-helper-maven-pluginaddssrc/generated/javaas a source rootsrc/generated/java/**Infrastructure
config/checkstyle/checkstyle.xml— excludesgeneratedandrpcpackages from Javadoc enforcement.gitattributes—src/generated/java/** eol=lf linguist-generated=true.github/workflows/codegen-check.yml— installs deps, re-runsnpm run generate, fails on any diff (runs onpush/pull_requestpaths that touch codegen or generated files).github/workflows/update-copilot-dependency.yml—workflow_dispatchwithversioninput; updates@github/copilotinscripts/codegen(vianpm installto updatepackage.jsonand lock file, thennpm cifor a clean reproducible install), regenerates, opens a PR automaticallyPull request checklist
mvn spotless:applyhas been run to format the codemvn clean verifypasses locallyDoes this introduce a breaking change?