Repository navigation
[Java] @CopilotTool ergonomics: Add schema attribute to @Param for custom type schema override #1794
Description
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
Stringas the parameter type and parse manually - Use a
java.timeor 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— 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. @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 withdefaultValue(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
@CopilotToolmethod 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 verifypasses 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. Theschemaattribute only overrides the type-derived schema; description, required, and name metadata still apply independently. - Does NOT affect
defaultValuesemantics. If the user needs a default, it should be expressed in the schema itself or viarequired = 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
mainonupstream - PR target:
main - Ensure
mvn spotless:applypasses before commit - Follow existing Javadoc conventions (required on public API additions)
Activity
[-]Java: Add schema attribute to @Param for custom type schema override[/-][+][Java] Add schema attribute to @Param for custom type schema override[/+][-][Java] Add schema attribute to @Param for custom type schema override[/-][+][Java] `@CopilotTool` ergonomics: Add schema attribute to @Param for custom type schema override[/+]java: add schema attribute to @CopilotToolParam and lambda for custom…
java: add schema attribute to @CopilotToolParam and lambda for custom…
java: add schema attribute to @CopilotToolParam and lambda for custom…