Overview
Add an optional schema attribute to the @Param annotation that allows users to explicitly specify the JSON Schema for a tool parameter. This enables support for custom/third-party types (e.g., internal date libraries, domain objects) that the SchemaGenerator cannot automatically map to a correct JSON Schema.
Parent Epic: #1682
Motivation
The SchemaGenerator handles a bounded set of well-known types (java.time.*, primitives, records, enums, etc.), but users may have internal libraries with custom types that:
- Fall through to the "class/object" schema path, exposing meaningless internal fields
- Cannot be automatically mapped to a meaningful JSON Schema for the LLM
Currently, the only workarounds are:
- Use
String as the parameter type and parse manually
- Use a
java.time or primitive type at the tool boundary and convert internally
A schema attribute on @Param gives users an explicit escape hatch.
Deliverables
Files to modify
java/src/main/java/com/github/copilot/tool/Param.java — add String schema() default "";
java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java — in generateSchemaWithParamMetadata(), check paramAnnotation.schema() before calling schemaGenerator.generateSchemaSource(). If non-empty, use the raw schema string directly.
java/src/main/java/com/github/copilot/tool/SchemaGenerator.java — no changes needed (schema override bypasses the generator entirely).
Implementation specification
1. @Param annotation change
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {
/** Parameter description (sent to the model). */
String value() default "";
/** Parameter name override. Defaults to the actual parameter name. */
String name() default "";
/** Whether this parameter is required. Default true. */
boolean required() default true;
/** Optional default value when the argument is omitted. */
String defaultValue() default "";
/**
* Optional explicit JSON Schema for this parameter as a JSON string literal.
* When non-empty, bypasses automatic schema generation from the parameter type.
* The value must be a valid JSON object string.
*
* <p>Example:
* <pre>
* @CopilotTool("Schedule meeting")
* public String schedule(
* @Param(value = "When to meet",
* schema = "{\"type\":\"string\",\"format\":\"date-time\"}")
* MyCustomDateTime when) {
* // ...
* }
* </pre>
*/
String schema() default "";
}
2. CopilotToolProcessor.generateSchemaWithParamMetadata() change
In the loop over parameters, before calling schemaGenerator.generateSchemaSource():
String typeSchema;
if (paramAnnotation != null && !paramAnnotation.schema().isEmpty()) {
// User-provided explicit schema — validate it is parseable JSON at compile time
try {
// Parse to validate — use javax.json or a simple brace-matching check
typeSchema = paramAnnotation.schema();
} catch (Exception e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Param schema must be valid JSON: " + e.getMessage(), param);
continue;
}
} else {
typeSchema = schemaGenerator.generateSchemaSource(paramType,
processingEnv.getTypeUtils(), processingEnv.getElementUtils());
}
Important: The schema value is a JSON string that will be emitted as a Map.of(...) expression in generated code. The processor must convert the JSON string to a Map.of(...) source code literal at compile time. One approach:
- Parse the JSON at compile time
- Generate nested
Map.of(key, value, ...) source expressions from the parsed structure
Alternatively, the generated code can embed the JSON and parse it at runtime:
// Generated code approach (simpler, slight runtime cost):
new com.fasterxml.jackson.databind.ObjectMapper().readValue(
"{\"type\":\"string\",\"format\":\"date-time\"}",
new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {})
The implementer should choose the approach that balances code complexity vs. runtime cost. The Map.of(...) compile-time approach is preferred for consistency with existing generated code.
3. Compile-time validation
The processor MUST emit a Diagnostic.Kind.ERROR if:
@Param(schema = "...") is non-empty AND the value is not valid JSON (missing braces, malformed)
@Param(schema = "...") is used together with defaultValue (conflicting metadata — the schema should contain the default if needed)
4. Interaction with description
When both schema and value (description) are specified, the description should still be added via withMeta(...) wrapping, just as it is today for auto-generated schemas.
Gating tests and criteria
All of the following must pass:
Unit tests (in SchemaGeneratorTest.java or new ParamSchemaOverrideTest.java)
- Explicit schema override: A
@CopilotTool method with @Param(schema = "{\"type\":\"string\",\"format\":\"date-time\"}") on a custom type produces the expected schema in generated code.
- Schema with description:
@Param(value = "When to meet", schema = "{\"type\":\"string\",\"format\":\"date-time\"}") produces schema with both format and description.
- Invalid schema compile error:
@Param(schema = "not json") emits a compile error.
- Schema + defaultValue conflict:
@Param(schema = "...", defaultValue = "x") emits a compile error.
- Empty schema falls through:
@Param(schema = "") (or omitted) uses normal type-based generation.
Integration test (in ToolDefinitionFromObjectTest.java)
- Handler invocation with custom type: Define a fixture with a custom type parameter, explicit schema, and verify the handler can be invoked and the ObjectMapper deserializes the argument correctly.
Existing tests must continue to pass
mvn clean verify passes with no regressions.
Design decisions
- No reflection fallback. The schema override is purely compile-time metadata.
- JSON validation at compile time. Invalid schema strings fail the build, not at runtime.
- Composable with
value/required. The schema attribute only overrides the type-derived schema; description, required, and name metadata still apply independently.
- Does NOT affect
defaultValue semantics. If the user needs a default, it should be expressed in the schema itself or via required = false.
Example usage
// Internal enterprise date library
@CopilotTool("Schedule a deployment")
public String scheduleDeployment(
@Param(value = "Deployment time in ISO-8601",
schema = "{\"type\":\"string\",\"format\":\"date-time\"}")
com.acme.internal.AcmeDateTime deployTime,
@Param("Target environment") String environment) {
// ObjectMapper with custom deserializer handles AcmeDateTime
return "Scheduled for " + deployTime + " on " + environment;
}
Branch and PR conventions
- Branch: create from
main on upstream
- PR target:
main
- Ensure
mvn spotless:apply passes before commit
- Follow existing Javadoc conventions (required on public API additions)
Overview
Add an optional
schemaattribute to the@Paramannotation that allows users to explicitly specify the JSON Schema for a tool parameter. This enables support for custom/third-party types (e.g., internal date libraries, domain objects) that theSchemaGeneratorcannot automatically map to a correct JSON Schema.Parent Epic: #1682
Motivation
The
SchemaGeneratorhandles a bounded set of well-known types (java.time.*, primitives, records, enums, etc.), but users may have internal libraries with custom types that:Currently, the only workarounds are:
Stringas the parameter type and parse manuallyjava.timeor primitive type at the tool boundary and convert internallyA
schemaattribute on@Paramgives users an explicit escape hatch.Deliverables
Files to modify
java/src/main/java/com/github/copilot/tool/Param.java— addString schema() default "";java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java— ingenerateSchemaWithParamMetadata(), checkparamAnnotation.schema()before callingschemaGenerator.generateSchemaSource(). If non-empty, use the raw schema string directly.java/src/main/java/com/github/copilot/tool/SchemaGenerator.java— no changes needed (schema override bypasses the generator entirely).Implementation specification
1.
@Paramannotation change2.
CopilotToolProcessor.generateSchemaWithParamMetadata()changeIn the loop over parameters, before calling
schemaGenerator.generateSchemaSource():Important: The
schemavalue is a JSON string that will be emitted as aMap.of(...)expression in generated code. The processor must convert the JSON string to aMap.of(...)source code literal at compile time. One approach:Map.of(key, value, ...)source expressions from the parsed structureAlternatively, the generated code can embed the JSON and parse it at runtime:
The implementer should choose the approach that balances code complexity vs. runtime cost. The
Map.of(...)compile-time approach is preferred for consistency with existing generated code.3. Compile-time validation
The processor MUST emit a
Diagnostic.Kind.ERRORif:@Param(schema = "...")is non-empty AND the value is not valid JSON (missing braces, malformed)@Param(schema = "...")is used together withdefaultValue(conflicting metadata — the schema should contain the default if needed)4. Interaction with
descriptionWhen both
schemaandvalue(description) are specified, the description should still be added viawithMeta(...)wrapping, just as it is today for auto-generated schemas.Gating tests and criteria
All of the following must pass:
Unit tests (in
SchemaGeneratorTest.javaor newParamSchemaOverrideTest.java)@CopilotToolmethod with@Param(schema = "{\"type\":\"string\",\"format\":\"date-time\"}")on a custom type produces the expected schema in generated code.@Param(value = "When to meet", schema = "{\"type\":\"string\",\"format\":\"date-time\"}")produces schema with both format and description.@Param(schema = "not json")emits a compile error.@Param(schema = "...", defaultValue = "x")emits a compile error.@Param(schema = "")(or omitted) uses normal type-based generation.Integration test (in
ToolDefinitionFromObjectTest.java)Existing tests must continue to pass
mvn clean verifypasses with no regressions.Design decisions
value/required. Theschemaattribute only overrides the type-derived schema; description, required, and name metadata still apply independently.defaultValuesemantics. If the user needs a default, it should be expressed in the schema itself or viarequired = false.Example usage
Branch and PR conventions
mainonupstreammainmvn spotless:applypasses before commit