diff --git a/java/mvnw b/java/mvnw
old mode 100644
new mode 100755
diff --git a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java
new file mode 100644
index 000000000..f2c92df85
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java
@@ -0,0 +1,384 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.tool;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.RecordComponentElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/**
+ * Compile-time utility that maps {@code javax.lang.model} types to JSON Schema
+ * represented as Java source code literals ({@code Map.of(...)} expressions).
+ *
+ *
+ * This class is invoked by the annotation processor and operates exclusively
+ * with the {@code javax.lang.model} API. It does NOT use
+ * {@code java.lang.reflect}.
+ *
+ * @since 1.0.2
+ */
+public class SchemaGenerator {
+
+ /**
+ * Given a {@link TypeMirror} from the annotation processing environment,
+ * returns a {@code String} containing Java source code for a {@code Map}
+ * literal representing the JSON Schema of that type.
+ *
+ * @param type
+ * the type to generate schema for
+ * @param typeUtils
+ * the {@link Types} utility from the processing environment
+ * @param elementUtils
+ * the {@link Elements} utility from the processing environment
+ * @return a Java source code string representing the JSON Schema
+ */
+ public String generateSchemaSource(TypeMirror type, Types typeUtils, Elements elementUtils) {
+ return generateSchema(type, typeUtils, elementUtils);
+ }
+
+ /**
+ * Generates the full "parameters" schema source for a method's parameters.
+ * Produces a
+ * {@code Map.of("type", "object", "properties", Map.of(...), "required", List.of(...))}.
+ *
+ * @param parameters
+ * the method parameters to generate schema for
+ * @param typeUtils
+ * the {@link Types} utility from the processing environment
+ * @param elementUtils
+ * the {@link Elements} utility from the processing environment
+ * @return a Java source code string representing the parameters JSON Schema
+ */
+ public String generateParametersSchemaSource(List extends VariableElement> parameters, Types typeUtils,
+ Elements elementUtils) {
+ if (parameters.isEmpty()) {
+ return "Map.of(\"type\", \"object\", \"properties\", Map.of(), \"required\", List.of())";
+ }
+
+ List propertyEntries = new ArrayList<>();
+ List requiredNames = new ArrayList<>();
+
+ for (VariableElement param : parameters) {
+ String paramName = param.getSimpleName().toString();
+ TypeMirror paramType = param.asType();
+
+ boolean isOptional = isOptionalType(paramType, typeUtils, elementUtils);
+ String schema;
+ if (isOptional) {
+ schema = generateSchema(unwrapOptional(paramType, typeUtils, elementUtils), typeUtils, elementUtils);
+ } else {
+ schema = generateSchema(paramType, typeUtils, elementUtils);
+ }
+
+ propertyEntries.add("Map.entry(\"" + paramName + "\", " + schema + ")");
+
+ if (!isOptional) {
+ Param paramAnnotation = param.getAnnotation(Param.class);
+ if (paramAnnotation == null || paramAnnotation.required()) {
+ requiredNames.add("\"" + paramName + "\"");
+ }
+ }
+ }
+
+ String properties = "Map.ofEntries(" + String.join(", ", propertyEntries) + ")";
+ String required = "List.of(" + String.join(", ", requiredNames) + ")";
+
+ return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")";
+ }
+
+ private String generateSchema(TypeMirror type, Types typeUtils, Elements elementUtils) {
+ // Handle primitive types
+ if (type.getKind().isPrimitive()) {
+ return generatePrimitiveSchema(type.getKind());
+ }
+
+ // Handle array types
+ if (type.getKind() == TypeKind.ARRAY) {
+ ArrayType arrayType = (ArrayType) type;
+ TypeMirror componentType = arrayType.getComponentType();
+ String itemsSchema = generateSchema(componentType, typeUtils, elementUtils);
+ return "Map.of(\"type\", \"array\", \"items\", " + itemsSchema + ")";
+ }
+
+ // Handle declared types (classes, interfaces, enums, records)
+ if (type.getKind() == TypeKind.DECLARED) {
+ return generateDeclaredTypeSchema((DeclaredType) type, typeUtils, elementUtils);
+ }
+
+ // Fallback: any
+ return "Map.of()";
+ }
+
+ private String generatePrimitiveSchema(TypeKind kind) {
+ switch (kind) {
+ case INT :
+ case LONG :
+ case BYTE :
+ case SHORT :
+ return "Map.of(\"type\", \"integer\")";
+ case DOUBLE :
+ case FLOAT :
+ return "Map.of(\"type\", \"number\")";
+ case BOOLEAN :
+ return "Map.of(\"type\", \"boolean\")";
+ case CHAR :
+ return "Map.of(\"type\", \"string\")";
+ default :
+ return "Map.of()";
+ }
+ }
+
+ private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, Elements elementUtils) {
+ TypeElement typeElement = (TypeElement) type.asElement();
+ String qualifiedName = typeElement.getQualifiedName().toString();
+
+ // String
+ if ("java.lang.String".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"string\")";
+ }
+
+ // Boxed primitives
+ if ("java.lang.Integer".equals(qualifiedName) || "java.lang.Long".equals(qualifiedName)
+ || "java.lang.Byte".equals(qualifiedName) || "java.lang.Short".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"integer\")";
+ }
+ if ("java.lang.Double".equals(qualifiedName) || "java.lang.Float".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"number\")";
+ }
+ if ("java.lang.Boolean".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"boolean\")";
+ }
+ if ("java.lang.Character".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"string\")";
+ }
+
+ // UUID
+ if ("java.util.UUID".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"string\", \"format\", \"uuid\")";
+ }
+
+ // OffsetDateTime
+ if ("java.time.OffsetDateTime".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"string\", \"format\", \"date-time\")";
+ }
+
+ // JsonNode (any)
+ if ("com.fasterxml.jackson.databind.JsonNode".equals(qualifiedName)) {
+ return "Map.of()";
+ }
+
+ // Object (any)
+ if ("java.lang.Object".equals(qualifiedName)) {
+ return "Map.of()";
+ }
+
+ // Optional types
+ if ("java.util.Optional".equals(qualifiedName)) {
+ List extends TypeMirror> typeArgs = type.getTypeArguments();
+ if (!typeArgs.isEmpty()) {
+ return generateSchema(typeArgs.get(0), typeUtils, elementUtils);
+ }
+ return "Map.of()";
+ }
+ if ("java.util.OptionalInt".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"integer\")";
+ }
+ if ("java.util.OptionalDouble".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"number\")";
+ }
+ if ("java.util.OptionalLong".equals(qualifiedName)) {
+ return "Map.of(\"type\", \"integer\")";
+ }
+
+ // List / Collection
+ if (isCollectionType(qualifiedName)) {
+ List extends TypeMirror> typeArgs = type.getTypeArguments();
+ if (!typeArgs.isEmpty()) {
+ String itemsSchema = generateSchema(typeArgs.get(0), typeUtils, elementUtils);
+ return "Map.of(\"type\", \"array\", \"items\", " + itemsSchema + ")";
+ }
+ return "Map.of(\"type\", \"array\")";
+ }
+
+ // Map
+ if (isMapType(qualifiedName)) {
+ List extends TypeMirror> typeArgs = type.getTypeArguments();
+ if (typeArgs.size() == 2) {
+ TypeMirror valueType = typeArgs.get(1);
+ if (valueType.getKind() == TypeKind.DECLARED) {
+ TypeElement valueElement = (TypeElement) ((DeclaredType) valueType).asElement();
+ String valueQName = valueElement.getQualifiedName().toString();
+ if ("java.lang.Object".equals(valueQName)) {
+ return "Map.of(\"type\", \"object\")";
+ }
+ }
+ String valueSchema = generateSchema(valueType, typeUtils, elementUtils);
+ return "Map.of(\"type\", \"object\", \"additionalProperties\", " + valueSchema + ")";
+ }
+ return "Map.of(\"type\", \"object\")";
+ }
+
+ // Enum types
+ if (typeElement.getKind() == ElementKind.ENUM) {
+ List constants = typeElement.getEnclosedElements().stream()
+ .filter(e -> e.getKind() == ElementKind.ENUM_CONSTANT)
+ .map(e -> "\"" + e.getSimpleName().toString() + "\"").collect(Collectors.toList());
+ return "Map.of(\"type\", \"string\", \"enum\", List.of(" + String.join(", ", constants) + "))";
+ }
+
+ // Record types
+ if (typeElement.getKind() == ElementKind.RECORD) {
+ return generateRecordSchema(typeElement, typeUtils, elementUtils);
+ }
+
+ // POJO / class types — treat as object with fields
+ if (typeElement.getKind() == ElementKind.CLASS) {
+ return generateClassSchema(typeElement, typeUtils, elementUtils);
+ }
+
+ // Sealed interfaces — oneOf via permitted subclasses
+ if (typeElement.getKind() == ElementKind.INTERFACE) {
+ return generateSealedSchema(typeElement, typeUtils, elementUtils);
+ }
+
+ return "Map.of()";
+ }
+
+ private String generateRecordSchema(TypeElement typeElement, Types typeUtils, Elements elementUtils) {
+ List propertyEntries = new ArrayList<>();
+ List requiredNames = new ArrayList<>();
+
+ for (Element enclosed : typeElement.getEnclosedElements()) {
+ if (enclosed.getKind() == ElementKind.RECORD_COMPONENT) {
+ RecordComponentElement component = (RecordComponentElement) enclosed;
+ String name = component.getSimpleName().toString();
+ TypeMirror componentType = component.asType();
+
+ boolean isOptional = isOptionalType(componentType, typeUtils, elementUtils);
+ String schema;
+ if (isOptional) {
+ schema = generateSchema(unwrapOptional(componentType, typeUtils, elementUtils), typeUtils,
+ elementUtils);
+ } else {
+ schema = generateSchema(componentType, typeUtils, elementUtils);
+ requiredNames.add("\"" + name + "\"");
+ }
+
+ propertyEntries.add("Map.entry(\"" + name + "\", " + schema + ")");
+ }
+ }
+
+ String properties = "Map.ofEntries(" + String.join(", ", propertyEntries) + ")";
+ String required = "List.of(" + String.join(", ", requiredNames) + ")";
+
+ return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")";
+ }
+
+ private String generateClassSchema(TypeElement typeElement, Types typeUtils, Elements elementUtils) {
+ List propertyEntries = new ArrayList<>();
+ List requiredNames = new ArrayList<>();
+
+ for (Element enclosed : typeElement.getEnclosedElements()) {
+ if (enclosed.getKind() == ElementKind.FIELD) {
+ VariableElement field = (VariableElement) enclosed;
+ // Skip static fields
+ if (field.getModifiers().contains(javax.lang.model.element.Modifier.STATIC)) {
+ continue;
+ }
+ String name = field.getSimpleName().toString();
+ TypeMirror fieldType = field.asType();
+
+ boolean isOptional = isOptionalType(fieldType, typeUtils, elementUtils);
+ String schema;
+ if (isOptional) {
+ schema = generateSchema(unwrapOptional(fieldType, typeUtils, elementUtils), typeUtils,
+ elementUtils);
+ } else {
+ schema = generateSchema(fieldType, typeUtils, elementUtils);
+ requiredNames.add("\"" + name + "\"");
+ }
+
+ propertyEntries.add("Map.entry(\"" + name + "\", " + schema + ")");
+ }
+ }
+
+ if (propertyEntries.isEmpty()) {
+ return "Map.of(\"type\", \"object\")";
+ }
+
+ String properties = "Map.ofEntries(" + String.join(", ", propertyEntries) + ")";
+ String required = "List.of(" + String.join(", ", requiredNames) + ")";
+
+ return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")";
+ }
+
+ private String generateSealedSchema(TypeElement typeElement, Types typeUtils, Elements elementUtils) {
+ List extends TypeMirror> permittedSubclasses = typeElement.getPermittedSubclasses();
+ if (permittedSubclasses != null && !permittedSubclasses.isEmpty()) {
+ List schemas = permittedSubclasses.stream().map(sub -> generateSchema(sub, typeUtils, elementUtils))
+ .collect(Collectors.toList());
+ return "Map.of(\"oneOf\", List.of(" + String.join(", ", schemas) + "))";
+ }
+ return "Map.of(\"type\", \"object\")";
+ }
+
+ private boolean isOptionalType(TypeMirror type, Types typeUtils, Elements elementUtils) {
+ if (type.getKind() != TypeKind.DECLARED) {
+ return false;
+ }
+ DeclaredType declaredType = (DeclaredType) type;
+ TypeElement element = (TypeElement) declaredType.asElement();
+ String name = element.getQualifiedName().toString();
+ return "java.util.Optional".equals(name) || "java.util.OptionalInt".equals(name)
+ || "java.util.OptionalDouble".equals(name) || "java.util.OptionalLong".equals(name);
+ }
+
+ private TypeMirror unwrapOptional(TypeMirror type, Types typeUtils, Elements elementUtils) {
+ if (type.getKind() != TypeKind.DECLARED) {
+ return type;
+ }
+ DeclaredType declaredType = (DeclaredType) type;
+ TypeElement element = (TypeElement) declaredType.asElement();
+ String name = element.getQualifiedName().toString();
+
+ if ("java.util.Optional".equals(name)) {
+ List extends TypeMirror> typeArgs = declaredType.getTypeArguments();
+ if (!typeArgs.isEmpty()) {
+ return typeArgs.get(0);
+ }
+ }
+ if ("java.util.OptionalInt".equals(name)) {
+ return typeUtils.getPrimitiveType(TypeKind.INT);
+ }
+ if ("java.util.OptionalDouble".equals(name)) {
+ return typeUtils.getPrimitiveType(TypeKind.DOUBLE);
+ }
+ if ("java.util.OptionalLong".equals(name)) {
+ return typeUtils.getPrimitiveType(TypeKind.LONG);
+ }
+ return type;
+ }
+
+ private boolean isCollectionType(String qualifiedName) {
+ return "java.util.List".equals(qualifiedName) || "java.util.Collection".equals(qualifiedName)
+ || "java.util.Set".equals(qualifiedName);
+ }
+
+ private boolean isMapType(String qualifiedName) {
+ return "java.util.Map".equals(qualifiedName);
+ }
+}
diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java
new file mode 100644
index 000000000..8e024ab9f
--- /dev/null
+++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java
@@ -0,0 +1,700 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.tool;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link SchemaGenerator} using the compilation-testing approach. A
+ * test annotation processor exercises SchemaGenerator during compilation of
+ * small source snippets.
+ */
+public class SchemaGeneratorTest {
+
+ /**
+ * In-memory Java source file for compilation testing.
+ */
+ private static class InMemorySource extends SimpleJavaFileObject {
+
+ private final String code;
+
+ InMemorySource(String className, String code) {
+ super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
+ this.code = code;
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+ return code;
+ }
+ }
+
+ /**
+ * Test processor that captures schema generation results.
+ */
+ @SupportedAnnotationTypes("*")
+ @SupportedSourceVersion(SourceVersion.RELEASE_17)
+ public static class SchemaCapturingProcessor extends AbstractProcessor {
+
+ static final List capturedSchemas = new ArrayList<>();
+ static final List capturedParameterSchemas = new ArrayList<>();
+
+ private Types typeUtils;
+ private Elements elementUtils;
+
+ @Override
+ public synchronized void init(ProcessingEnvironment processingEnv) {
+ super.init(processingEnv);
+ this.typeUtils = processingEnv.getTypeUtils();
+ this.elementUtils = processingEnv.getElementUtils();
+ }
+
+ @Override
+ public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ if (roundEnv.processingOver()) {
+ return false;
+ }
+
+ SchemaGenerator generator = new SchemaGenerator();
+
+ for (Element rootElement : roundEnv.getRootElements()) {
+ if (rootElement.getKind() == ElementKind.CLASS || rootElement.getKind() == ElementKind.RECORD
+ || rootElement.getKind() == ElementKind.INTERFACE
+ || rootElement.getKind() == ElementKind.ENUM) {
+ // Find methods named "schemaTarget" to capture schemas for their return type
+ for (Element enclosed : rootElement.getEnclosedElements()) {
+ if (enclosed.getKind() == ElementKind.METHOD) {
+ ExecutableElement method = (ExecutableElement) enclosed;
+ String methodName = method.getSimpleName().toString();
+ if (methodName.startsWith("schemaTarget")) {
+ TypeMirror returnType = method.getReturnType();
+ String schema = generator.generateSchemaSource(returnType, typeUtils, elementUtils);
+ capturedSchemas.add(methodName + "=" + schema);
+ }
+ if ("parametersTarget".equals(methodName)) {
+ List extends VariableElement> params = method.getParameters();
+ String schema = generator.generateParametersSchemaSource(params, typeUtils,
+ elementUtils);
+ capturedParameterSchemas.add(schema);
+ }
+ }
+ }
+
+ // For record/enum types, generate schema for the type itself
+ TypeElement typeElement = (TypeElement) rootElement;
+ String typeName = typeElement.getSimpleName().toString();
+ if (typeName.startsWith("TestRecord") || typeName.startsWith("TestEnum")
+ || typeName.startsWith("TestSealed")) {
+ String schema = generator.generateSchemaSource(typeElement.asType(), typeUtils, elementUtils);
+ capturedSchemas.add(typeName + "=" + schema);
+ }
+ }
+ }
+
+ return false;
+ }
+ }
+
+ private static final Path CLASS_OUTPUT_DIR = Path.of("target", "test-schema-classes");
+
+ /**
+ * Creates a StandardJavaFileManager that writes compiled .class files to
+ * target/test-schema-classes/ instead of the working directory.
+ */
+ private StandardJavaFileManager createFileManager(JavaCompiler compiler,
+ DiagnosticCollector diagnostics) throws IOException {
+ Files.createDirectories(CLASS_OUTPUT_DIR);
+ StandardJavaFileManager fm = compiler.getStandardFileManager(diagnostics, null, null);
+ fm.setLocation(StandardLocation.CLASS_OUTPUT, List.of(CLASS_OUTPUT_DIR.toFile()));
+ return fm;
+ }
+
+ private List compileAndCapture(String... sources) {
+ return compileAndCapture(Arrays.asList(sources));
+ }
+
+ private List compileAndCapture(List sourceTexts) {
+ SchemaCapturingProcessor.capturedSchemas.clear();
+ SchemaCapturingProcessor.capturedParameterSchemas.clear();
+
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ assertNotNull(compiler, "System Java compiler not available");
+
+ DiagnosticCollector diagnostics = new DiagnosticCollector<>();
+
+ List compilationUnits = new ArrayList<>();
+ for (String sourceText : sourceTexts) {
+ // Extract class name from source
+ String className = extractClassName(sourceText);
+ compilationUnits.add(new InMemorySource(className, sourceText));
+ }
+
+ try (StandardJavaFileManager fm = createFileManager(compiler, diagnostics)) {
+ // Compile with the processor on classpath
+ JavaCompiler.CompilationTask task = compiler.getTask(null, // writer
+ fm, // file manager
+ diagnostics, // diagnostics
+ List.of("--add-modules", "ALL-MODULE-PATH"), // options
+ null, // annotation classes
+ compilationUnits);
+
+ task.setProcessors(List.of(new SchemaCapturingProcessor()));
+ boolean success = task.call();
+
+ if (!success) {
+ // Try without module options for simpler environments
+ diagnostics = new DiagnosticCollector<>();
+ try (StandardJavaFileManager fm2 = createFileManager(compiler, diagnostics)) {
+ task = compiler.getTask(null, fm2, diagnostics, null, null, compilationUnits);
+ task.setProcessors(List.of(new SchemaCapturingProcessor()));
+ success = task.call();
+ }
+ }
+
+ assertTrue(success, "Compilation failed: " + diagnostics.getDiagnostics());
+ } catch (IOException e) {
+ fail("Failed to create file manager: " + e.getMessage());
+ }
+ return new ArrayList<>(SchemaCapturingProcessor.capturedSchemas);
+ }
+
+ private List compileAndCaptureParams(String source) {
+ SchemaCapturingProcessor.capturedSchemas.clear();
+ SchemaCapturingProcessor.capturedParameterSchemas.clear();
+
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ assertNotNull(compiler, "System Java compiler not available");
+
+ DiagnosticCollector diagnostics = new DiagnosticCollector<>();
+
+ String className = extractClassName(source);
+ List compilationUnits = List.of(new InMemorySource(className, source));
+
+ try (StandardJavaFileManager fm = createFileManager(compiler, diagnostics)) {
+ JavaCompiler.CompilationTask task = compiler.getTask(null, fm, diagnostics, null, null, compilationUnits);
+ task.setProcessors(List.of(new SchemaCapturingProcessor()));
+ boolean success = task.call();
+
+ assertTrue(success, "Compilation failed: " + diagnostics.getDiagnostics());
+ } catch (IOException e) {
+ fail("Failed to create file manager: " + e.getMessage());
+ }
+ return new ArrayList<>(SchemaCapturingProcessor.capturedParameterSchemas);
+ }
+
+ private String extractClassName(String source) {
+ // Simple extraction: find "class X", "record X", "enum X", or "interface X"
+ for (String keyword : new String[]{"class ", "record ", "enum ", "interface "}) {
+ int idx = source.indexOf(keyword);
+ if (idx >= 0) {
+ int start = idx + keyword.length();
+ int end = start;
+ while (end < source.length() && Character.isJavaIdentifierPart(source.charAt(end))) {
+ end++;
+ }
+ return source.substring(start, end);
+ }
+ }
+ return "Unknown";
+ }
+
+ // --- Type mapping tests ---
+
+ @Test
+ void stringType() {
+ String source = """
+ public class TestStringHolder {
+ public String schemaTargetString() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetString", "Map.of(\"type\", \"string\")");
+ }
+
+ @Test
+ void intPrimitiveType() {
+ String source = """
+ public class TestIntHolder {
+ public int schemaTargetInt() { return 0; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetInt", "Map.of(\"type\", \"integer\")");
+ }
+
+ @Test
+ void integerBoxedType() {
+ String source = """
+ public class TestIntegerHolder {
+ public Integer schemaTargetInteger() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetInteger", "Map.of(\"type\", \"integer\")");
+ }
+
+ @Test
+ void longType() {
+ String source = """
+ public class TestLongHolder {
+ public long schemaTargetLong() { return 0L; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetLong", "Map.of(\"type\", \"integer\")");
+ }
+
+ @Test
+ void doubleType() {
+ String source = """
+ public class TestDoubleHolder {
+ public double schemaTargetDouble() { return 0.0; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetDouble", "Map.of(\"type\", \"number\")");
+ }
+
+ @Test
+ void floatType() {
+ String source = """
+ public class TestFloatHolder {
+ public float schemaTargetFloat() { return 0.0f; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetFloat", "Map.of(\"type\", \"number\")");
+ }
+
+ @Test
+ void booleanPrimitiveType() {
+ String source = """
+ public class TestBooleanHolder {
+ public boolean schemaTargetBoolean() { return false; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetBoolean", "Map.of(\"type\", \"boolean\")");
+ }
+
+ @Test
+ void booleanBoxedType() {
+ String source = """
+ public class TestBooleanBoxedHolder {
+ public Boolean schemaTargetBooleanBoxed() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetBooleanBoxed", "Map.of(\"type\", \"boolean\")");
+ }
+
+ @Test
+ void byteBoxedType() {
+ String source = """
+ public class TestByteHolder {
+ public Byte schemaTargetByte() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetByte", "Map.of(\"type\", \"integer\")");
+ }
+
+ @Test
+ void shortBoxedType() {
+ String source = """
+ public class TestShortHolder {
+ public Short schemaTargetShort() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetShort", "Map.of(\"type\", \"integer\")");
+ }
+
+ @Test
+ void characterBoxedType() {
+ String source = """
+ public class TestCharHolder {
+ public Character schemaTargetChar() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetChar", "Map.of(\"type\", \"string\")");
+ }
+
+ @Test
+ void stringArrayType() {
+ String source = """
+ public class TestArrayHolder {
+ public String[] schemaTargetArray() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetArray",
+ "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\"))");
+ }
+
+ @Test
+ void enumType() {
+ String source = """
+ public enum TestEnumColor { RED, GREEN, BLUE }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "TestEnumColor",
+ "Map.of(\"type\", \"string\", \"enum\", List.of(\"RED\", \"GREEN\", \"BLUE\"))");
+ }
+
+ @Test
+ void listOfStringType() {
+ String source = """
+ import java.util.List;
+ public class TestListHolder {
+ public List schemaTargetList() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetList",
+ "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\"))");
+ }
+
+ @Test
+ void mapStringStringType() {
+ String source = """
+ import java.util.Map;
+ public class TestMapHolder {
+ public Map schemaTargetMap() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetMap",
+ "Map.of(\"type\", \"object\", \"additionalProperties\", Map.of(\"type\", \"string\"))");
+ }
+
+ @Test
+ void mapStringObjectType() {
+ String source = """
+ import java.util.Map;
+ public class TestMapObjectHolder {
+ public Map schemaTargetMapObject() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetMapObject", "Map.of(\"type\", \"object\")");
+ }
+
+ @Test
+ void mapStringBooleanType() {
+ String source = """
+ import java.util.Map;
+ public class TestMapBoolHolder {
+ public Map schemaTargetMapBool() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetMapBool",
+ "Map.of(\"type\", \"object\", \"additionalProperties\", Map.of(\"type\", \"boolean\"))");
+ }
+
+ @Test
+ void mapStringLongType() {
+ String source = """
+ import java.util.Map;
+ public class TestMapLongHolder {
+ public Map schemaTargetMapLong() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetMapLong",
+ "Map.of(\"type\", \"object\", \"additionalProperties\", Map.of(\"type\", \"integer\"))");
+ }
+
+ @Test
+ void optionalStringType() {
+ String source = """
+ import java.util.Optional;
+ public class TestOptionalHolder {
+ public Optional schemaTargetOptional() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetOptional", "Map.of(\"type\", \"string\")");
+ }
+
+ @Test
+ void optionalIntType() {
+ String source = """
+ import java.util.OptionalInt;
+ public class TestOptionalIntHolder {
+ public OptionalInt schemaTargetOptionalInt() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetOptionalInt", "Map.of(\"type\", \"integer\")");
+ }
+
+ @Test
+ void optionalLongType() {
+ String source = """
+ import java.util.OptionalLong;
+ public class TestOptionalLongHolder {
+ public OptionalLong schemaTargetOptionalLong() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetOptionalLong", "Map.of(\"type\", \"integer\")");
+ }
+
+ @Test
+ void optionalDoubleType() {
+ String source = """
+ import java.util.OptionalDouble;
+ public class TestOptionalDoubleHolder {
+ public OptionalDouble schemaTargetOptionalDouble() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetOptionalDouble", "Map.of(\"type\", \"number\")");
+ }
+
+ @Test
+ void uuidType() {
+ String source = """
+ import java.util.UUID;
+ public class TestUuidHolder {
+ public UUID schemaTargetUuid() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetUuid", "Map.of(\"type\", \"string\", \"format\", \"uuid\")");
+ }
+
+ @Test
+ void offsetDateTimeType() {
+ String source = """
+ import java.time.OffsetDateTime;
+ public class TestDateTimeHolder {
+ public OffsetDateTime schemaTargetDateTime() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetDateTime",
+ "Map.of(\"type\", \"string\", \"format\", \"date-time\")");
+ }
+
+ @Test
+ void recordType() {
+ String source = """
+ public record TestRecordPerson(String name, int age, boolean active) {}
+ """;
+ List schemas = compileAndCapture(source);
+ String expected = "Map.of(\"type\", \"object\", \"properties\", "
+ + "Map.ofEntries(Map.entry(\"name\", Map.of(\"type\", \"string\")), "
+ + "Map.entry(\"age\", Map.of(\"type\", \"integer\")), "
+ + "Map.entry(\"active\", Map.of(\"type\", \"boolean\"))), "
+ + "\"required\", List.of(\"name\", \"age\", \"active\"))";
+ assertContainsSchema(schemas, "TestRecordPerson", expected);
+ }
+
+ @Test
+ void recordWithOptionalField() {
+ String source = """
+ import java.util.Optional;
+ public record TestRecordWithOptional(String name, Optional nickname) {}
+ """;
+ List schemas = compileAndCapture(source);
+ String expected = "Map.of(\"type\", \"object\", \"properties\", "
+ + "Map.ofEntries(Map.entry(\"name\", Map.of(\"type\", \"string\")), "
+ + "Map.entry(\"nickname\", Map.of(\"type\", \"string\"))), " + "\"required\", List.of(\"name\"))";
+ assertContainsSchema(schemas, "TestRecordWithOptional", expected);
+ }
+
+ @Test
+ void recordWithMoreThanTenFields() {
+ String source = """
+ public record TestRecordLarge(
+ String f1, String f2, String f3, String f4, String f5,
+ String f6, String f7, String f8, String f9, String f10,
+ String f11) {}
+ """;
+ List schemas = compileAndCapture(source);
+ // Verify the schema contains all 11 fields and uses Map.ofEntries
+ String schema = schemas.stream().filter(s -> s.startsWith("TestRecordLarge=")).findFirst().orElse("");
+ assertFalse(schema.isEmpty(), "Expected schema for TestRecordLarge");
+ assertTrue(schema.contains("Map.ofEntries("), "Should use Map.ofEntries for >10 fields: " + schema);
+ assertTrue(schema.contains("Map.entry(\"f1\""), "Should have f1: " + schema);
+ assertTrue(schema.contains("Map.entry(\"f11\""), "Should have f11: " + schema);
+ // Verify the generated source expression is compilable by re-compiling it
+ String schemaExpr = schema.substring(schema.indexOf('=') + 1);
+ String validationSource = "import java.util.Map;\nimport java.util.List;\n"
+ + "public class LargeRecordValidation {\n" + " @SuppressWarnings(\"unchecked\")\n"
+ + " public Object schema() { return " + schemaExpr + "; }\n}\n";
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ DiagnosticCollector diagnostics = new DiagnosticCollector<>();
+ List units = List.of(new InMemorySource("LargeRecordValidation", validationSource));
+ try (StandardJavaFileManager fm = createFileManager(compiler, diagnostics)) {
+ JavaCompiler.CompilationTask task = compiler.getTask(null, fm, diagnostics, null, null, units);
+ boolean success = task.call();
+ assertTrue(success, "Generated schema for >10-field record does not compile: "
+ + diagnostics.getDiagnostics() + "\nSource:\n" + validationSource);
+ } catch (IOException e) {
+ fail("Failed to create file manager: " + e.getMessage());
+ }
+ }
+
+ @Test
+ void parametersSchema() {
+ String source = """
+ public class TestParamsHolder {
+ public void parametersTarget(String query, int limit, boolean verbose) {}
+ }
+ """;
+ List paramSchemas = compileAndCaptureParams(source);
+ assertFalse(paramSchemas.isEmpty(), "Expected parameter schemas");
+ String schema = paramSchemas.get(0);
+ assertTrue(schema.contains("\"type\", \"object\""), "Should be object type: " + schema);
+ assertTrue(schema.contains("Map.entry(\"query\", Map.of(\"type\", \"string\"))"),
+ "Should have query property: " + schema);
+ assertTrue(schema.contains("Map.entry(\"limit\", Map.of(\"type\", \"integer\"))"),
+ "Should have limit property: " + schema);
+ assertTrue(schema.contains("Map.entry(\"verbose\", Map.of(\"type\", \"boolean\"))"),
+ "Should have verbose property: " + schema);
+ assertTrue(schema.contains("\"required\", List.of("), "Should have required list: " + schema);
+ }
+
+ @Test
+ void generatedSourceIsValidJava() {
+ // Verify that generated schema source code compiles when embedded in a method
+ // body
+ String source = """
+ import java.util.List;
+ import java.util.Map;
+ import java.util.Optional;
+ public class TestValidJavaHolder {
+ public String schemaTargetStr() { return null; }
+ public List schemaTargetListStr() { return null; }
+ public Map schemaTargetMapStr() { return null; }
+ public Optional schemaTargetOpt() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertFalse(schemas.isEmpty());
+
+ // Build a Java source that uses the generated schema expressions
+ StringBuilder validationSource = new StringBuilder();
+ validationSource.append("import java.util.Map;\n");
+ validationSource.append("import java.util.List;\n");
+ validationSource.append("public class SchemaValidation {\n");
+ validationSource.append(" @SuppressWarnings(\"unchecked\")\n");
+ validationSource.append(" public void validate() {\n");
+ for (int i = 0; i < schemas.size(); i++) {
+ String schema = schemas.get(i);
+ String schemaExpr = schema.substring(schema.indexOf('=') + 1);
+ validationSource.append(" Object s" + i + " = " + schemaExpr + ";\n");
+ }
+ validationSource.append(" }\n");
+ validationSource.append("}\n");
+
+ // Compile the validation source to verify syntactic validity
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ DiagnosticCollector diagnostics = new DiagnosticCollector<>();
+ List compilationUnits = List
+ .of(new InMemorySource("SchemaValidation", validationSource.toString()));
+
+ try (StandardJavaFileManager fm = createFileManager(compiler, diagnostics)) {
+ JavaCompiler.CompilationTask task = compiler.getTask(null, fm, diagnostics, null, null, compilationUnits);
+ boolean success = task.call();
+
+ assertTrue(success, "Generated schema source code is not valid Java: " + diagnostics.getDiagnostics()
+ + "\nSource:\n" + validationSource);
+ } catch (IOException e) {
+ fail("Failed to create file manager: " + e.getMessage());
+ }
+ }
+
+ @Test
+ void nestedMapListType() {
+ String source = """
+ import java.util.List;
+ import java.util.Map;
+ public class TestNestedHolder {
+ public Map> schemaTargetNestedMap() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ String expected = "Map.of(\"type\", \"object\", \"additionalProperties\", "
+ + "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\")))";
+ assertContainsSchema(schemas, "schemaTargetNestedMap", expected);
+ }
+
+ @Test
+ void objectType() {
+ String source = """
+ public class TestObjectHolder {
+ public Object schemaTargetObject() { return null; }
+ }
+ """;
+ List schemas = compileAndCapture(source);
+ assertContainsSchema(schemas, "schemaTargetObject", "Map.of()");
+ }
+
+ @Test
+ void sealedInterfaceType() {
+ String sealedInterface = """
+ public sealed interface TestSealedShape permits TestSealedCircle, TestSealedRect {}
+ """;
+ String circle = """
+ public record TestSealedCircle(double radius) implements TestSealedShape {}
+ """;
+ String rect = """
+ public record TestSealedRect(double width, double height) implements TestSealedShape {}
+ """;
+ List schemas = compileAndCapture(sealedInterface, circle, rect);
+ String expected = "Map.of(\"oneOf\", List.of(" + "Map.of(\"type\", \"object\", \"properties\", "
+ + "Map.ofEntries(Map.entry(\"radius\", Map.of(\"type\", \"number\"))), "
+ + "\"required\", List.of(\"radius\")), " + "Map.of(\"type\", \"object\", \"properties\", "
+ + "Map.ofEntries(Map.entry(\"width\", Map.of(\"type\", \"number\")), "
+ + "Map.entry(\"height\", Map.of(\"type\", \"number\"))), "
+ + "\"required\", List.of(\"width\", \"height\"))))";
+ assertContainsSchema(schemas, "TestSealedShape", expected);
+ }
+
+ private void assertContainsSchema(List schemas, String methodName, String expectedSchema) {
+ String expected = methodName + "=" + expectedSchema;
+ assertTrue(schemas.stream().anyMatch(s -> s.equals(expected)),
+ "Expected schema '" + expected + "' not found in: " + schemas);
+ }
+}