
Javadoc, a renowned documentation generator for Java, proves invaluable when delving into unfamiliar source code, streamlining the onboarding process and aiding enhancements. However, the incremental nature of development introduces a challenge: refactoring can render initial Javadoc entries obsolete, necessitating frequent updates—an expensive undertaking in terms of time and effort.
Outdated Javadoc might overlook recent code nuances, potentially leading to misinterpretations for the reader. Corporate source code, with its generic comments and sometimes boilerplate Javadoc, exacerbates this issue, with documentation offering minimal additional valu. Addressing these challenges, we introduce an AI-driven tool designed to dynamically generate and regenerate pertinent, up-to-date Javadoc. This solution aims to reduce the documentation overhead for developers, ensuring that as code evolves, its accompanying Javadoc remains relevant and enlightening.
Enter ChatGPT
ChatGPT, based on OpenAI’s advanced GPT-4 architecture, is a prime candidate to revolutionize Javadoc generation. Boasting an extensive training set, it is proficient in understanding complex coding constructs and linguistic nuances, enabling it to derive context and generate meaningful documentation. Unlike conventional tools, ChatGPT’s ability to process and generate natural language content is exceptional, ensuring the generated Javadoc is both technically accurate and comprehensible.

It certainly is conceivable, but not practical to do this manually method by method, class by class. To improve on this, one could use plugins, like https://www.codegpt.co/ to do it directly from the VS Code, but this still involves one to orchestrate the process.
We will implement a tool, that
- can be called from the command line, with a directory as a root
- it can be configured to generate class or method level javadoc
- and it would lend itself for further modifications for the purpose of the user
To fast forward to the runnable solution refer to this GitLab project.
To see, how we integrate the OpenAI Completions API and JavaParser to a running solution, keep on reading.
The OpenAI Completions API
The OpenAI Completions API allows developers to integrate OpenAI’s GPT models into applications, products, or services. It offers real-time text generation capabilities, enabling tasks like doc creation. We will not interested in its chat functionality, but will focus on its persona setup (system), example (user/assistant example) and user query.
Please note that using the OpenAI api is not free of charge. At the time of writing you would still get $5 credit at signup, sufficient to test this out, and document approx 10 Java Classes. The best way to test it yourself the AI part is to play around on the Playground.
It is also the playground, where you can test out the best prompts and settings for your use case. In our case it looks like this.

The most important output of the playground is the code which can be used in java.
Line 2 sets up the model, and 3.5 is a good compromise in speed/price and quality for our purposes.
Lines 6 and 7 setting up the persona of the assistant.
Lines 10 and 14, comprise a sample conversation.
Line 23 optimises the model, in that it will stop, if is starts repeating the user input, something the model has a tendency to do frequently.
{
"model": "gpt-3.5-turbo",
"messages": [
{
"role": "system",
"content": "You will be provided with the source code of a java class or java interface. Your task is to generate javadoc for this class or interface. The javadoc must be generated for the class or interface level, and not on the method level. \nDo not generate code comments.\nDo not print out the source code, that has been provided as input.\n\n"
},
{
"role": "user",
"content": "public class JavaDocAnalyzer {... omitted in this example}"
},
{
"role": "assistant",
"content": "/**\n * The JavaDocAnalyzer class is responsible for analyzing Java source code and generating Javadoc for classes\n * and interfaces that are missing it.\n */"
}
],
"temperature": 1,
"max_tokens": 150,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0,
"stop": ["public class", "public interface"]
}
In addition to the above, we also need to specify a Bearer Authorization header, as explained here: https://platform.openai.com/docs/quickstart/step-2-setup-your-api-key.
AST JavaParser
JavaParser’s AST (Abstract Syntax Tree) provides a structured representation of Java source code, enabling us to analyze, modify, or generate Java code programmatically. JavaParser translates source code into a navigable tree structure, facilitating code introspection (check JavaDoc exists), code extraction (send code to Completions API) and transformation (adding Docs).
At minimum, we will need to
// load the file from storage
FileInputStream in = new FileInputStream("MyClass.java");
// parse into Abstract Syntax Tree
CompilationUnit cu = JavaParser.parse(in);
// for each class in the loaded syntax
cu.findAll(ClassOrInterfaceDeclaration.class).forEach(classOrInterface -> {
// check if Javadoc exists
if (classOrInterface.getComment().isPresent() && classOrInterface.getComment().get() instanceof JavadocComment) {
System.out.println(classOrInterface.getName() + " has JavaDoc.");
} else {
// or needs to be added
System.out.println(classOrInterface.getName() + " does not have JavaDoc.");
}
});
We will also use the LexicalPreservingPrinter to maintain the formatting information, as by default the parser only maintains the syntax.
Here is the complete code, which is available with README and execution wrapper in GitHub
Full code of the solution
@SpringBootApplication
@Slf4j
public class DocWriterApplication implements CommandLineRunner {
@Value("${srcDir:}")
private String srcDir;
@Value("${author:DocWriterApplication}")
private String author;
@Value("${maxFileToChange:1}")
private int maxFileToChange;
private String openaiApiKey = System.getenv("OPENAI_API_KEY");
@Value("${classDoc:true}")
private boolean classDoc;
@Value("${publicMethodDoc:false}")
private boolean publicMethodDoc;
@Value("${nonPublicMethodDoc:false}")
private boolean nonPublicMethodDoc;
public static void main(String[] args) {
SpringApplication.run(DocWriterApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
logArgs();
addDocs();
}
private void logArgs() {
log.info("srcDir: " + srcDir);
log.info("maxFileToChange: " + maxFileToChange);
log.debug("openApiToken: " + openaiApiKey);
log.info("classDoc: " + classDoc);
log.info("publicMethodDoc: " + publicMethodDoc);
log.info("nonPublicMethodDoc: " + nonPublicMethodDoc);
}
private void addDocs() throws Exception {
// Create a SourceRoot
SourceRoot sourceRoot = new SourceRoot(Paths.get(srcDir));
// Parse all the Java files in the source root
List<ParseResult<CompilationUnit>> parseResults = sourceRoot.tryToParse("");
int changeCounter = maxFileToChange;
// Analyze each parsed result
for (ParseResult<CompilationUnit> parseResult : parseResults) {
if (parseResult.isSuccessful()) {
CompilationUnit cu = parseResult.getResult().get();
// this will try to preserve the format
LexicalPreservingPrinter.setup(cu);
log.info("Processing " + cu.getStorage().get().getPath());
boolean fileChanged = false;
// process only the top element of the file
ClassOrInterfaceDeclaration topClassOrInterface = cu
.findFirst(ClassOrInterfaceDeclaration.class).get();
if (topClassOrInterface.getComment().isEmpty() && this.classDoc) {
addClassJavadoc(cu, topClassOrInterface);
fileChanged = true;
}
// Process public methods if the top-level element is a class
if (this.publicMethodDoc || this.nonPublicMethodDoc) {
if (!topClassOrInterface.isInterface()) {
for (MethodDeclaration method : cu.findAll(MethodDeclaration.class)) {
if (method.getComment().isEmpty()
&&
((method.isPublic() && this.publicMethodDoc)
||
(!method.isPublic() && this.nonPublicMethodDoc))) {
addMethodJavadoc(cu, method);
fileChanged = true;
}
}
}
}
if (fileChanged){
changeCounter--;
save(cu);
}
}
if (changeCounter <= 0) {
return;
}
}
}
private void addMethodJavadoc(CompilationUnit cu, MethodDeclaration method) throws Exception {
log.info("Adding missing JavaDoc for method: " + method.getNameAsString());
String sourceCode = method.toString();
String generatedJavadoc = generateJavaDoc(sourceCode, setupMethodDocGeneration());
if (generatedJavadoc == null){
log.error("Failed to generate javadoc for {}", method.getNameAsString());
return;
}
Javadoc javadoc = new Javadoc(JavadocDescription.parseText(generatedJavadoc));
JavadocComment javadocComment = new JavadocComment(javadoc.toText());
method.setJavadocComment(javadocComment);
}
private void addClassJavadoc(CompilationUnit cu, ClassOrInterfaceDeclaration classOrInterface) throws Exception {
log.info("Adding missing JavaDoc for class/interface: " + classOrInterface.getNameAsString());
String sourceCode = "";
for (Node child : cu.getChildNodes()) {
if (!(child instanceof com.github.javaparser.ast.ImportDeclaration)) {
sourceCode += child.toString();
}
}
String generatedJavadoc = generateJavaDoc(sourceCode, setupClassDocGeneration());
if (generatedJavadoc == null){
log.error("Failed to generate javadoc for {}", classOrInterface.getNameAsString());
return;
}
Javadoc javadoc = new Javadoc(JavadocDescription.parseText(generatedJavadoc));
javadoc.addBlockTag(new JavadocBlockTag(JavadocBlockTag.Type.AUTHOR, author));
JavadocComment javadocComment = new JavadocComment(javadoc.toText());
classOrInterface.setJavadocComment(javadocComment);
}
private void save(CompilationUnit cu) throws IOException {
Optional<Storage> storage = cu.getStorage();
if (storage.isPresent()) {
Path pathToFile = storage.get().getPath();
log.info("Writing docs to " + pathToFile);
String code = LexicalPreservingPrinter.print(cu);
Files.write(pathToFile, code.getBytes(StandardCharsets.UTF_8));
}
}
private String generateJavaDoc(String classSourceCode, JSONObject messageBody) throws Exception {
log.trace("generating javadoc for \n" + classSourceCode);
JSONObject userMessage = new JSONObject();
userMessage.put("role", "user");
userMessage.put("content", classSourceCode);
messageBody.getJSONArray("messages").put(userMessage);
messageBody.put("model", "gpt-3.5-turbo");
messageBody.put("temperature", 1);
messageBody.put("max_tokens", 256);
messageBody.put("top_p", 1);
messageBody.put("frequency_penalty", 0);
messageBody.put("presence_penalty", 0);
log.info("Sending ChatGpt request ");
// Create HTTP request
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://api.openai.com/v1/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + openaiApiKey)
.POST(HttpRequest.BodyPublishers.ofString(messageBody.toString(), StandardCharsets.UTF_8))
.build();
// Send the request
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug("Received response {}", response.body());
// Process the response
JSONObject jsonResponse = new JSONObject(response.body());
if (jsonResponse.has("error")){
throw new RuntimeException(jsonResponse.getJSONObject("error").toString());
}
String content = jsonResponse.getJSONArray("choices").getJSONObject(0).getJSONObject("message")
.getString("content");
log.info("ChatGpt response received: ");
String classJavaDoc = extractJavadoc(content);
log.info(classJavaDoc);
return classJavaDoc;
}
private JSONObject setupMethodDocGeneration() {
JSONArray messagesArray = new JSONArray();
JSONObject systemMessage = new JSONObject();
systemMessage.put("role", "system");
systemMessage.put("content",
"You will be provided with java source code. Your task is to generate javadoc for this java method. \nDo not generate code comments.\nDo not print out the source code, that has been provided as input, merely the Javadoc for the method starting with the \n/**\nand ending with the\n/*\n");
messagesArray.put(systemMessage);
JSONObject messageBody = new JSONObject();
messageBody.put("messages", messagesArray);
return messageBody; }
private JSONObject setupClassDocGeneration() {
JSONArray messagesArray = new JSONArray();
JSONObject systemMessage = new JSONObject();
systemMessage.put("role", "system");
systemMessage.put("content",
"You will be provided with the source code of a java class or java interface. Your task is to generate javadoc for this class or interface. The javadoc must be generated for the class or interface level, and not on the method level. \\n" + //
"Do not generate code comments.\\n" + //
"Do not print out the source code, that has been provided as input.");
messagesArray.put(systemMessage);
JSONObject sampleUserMessage = new JSONObject();
sampleUserMessage.put("role", "user");
sampleUserMessage.put("content",
"public class JavaDocAnalyzer {\n\n private void main(String[] args) throws IOException {\n // Define the path to the source code\n String pathToSrc = \"src/main/java\";\n\n // Create a SourceRoot\n SourceRoot sourceRoot = new SourceRoot(Paths.get(pathToSrc));\n\n // Parse all the Java files in the source root\n List<ParseResult<CompilationUnit>> parseResults = sourceRoot.tryToParse(\"\");\n\n // Analyze each parsed result\n for (ParseResult<CompilationUnit> parseResult : parseResults) {\n if (parseResult.isSuccessful()) {\n CompilationUnit cu = parseResult.getResult().get();\n checkForMissingJavaDoc(cu);\n }\n }\n }\n\n private static void checkForMissingJavaDoc(CompilationUnit cu) throws Exception {\n for (ClassOrInterfaceDeclaration classOrInterface : cu.findAll(ClassOrInterfaceDeclaration.class)) {\n // Check if the class/interface has JavaDoc\n if (!classOrInterface.getComment().isPresent()) {\n log.info(\"Adding missing JavaDoc for class/interface: \" + classOrInterface.getNameAsString());\n \n String sourceCode = \"\";\n for (Node child : cu.getChildNodes()) {\n if (!(child instanceof com.github.javaparser.ast.ImportDeclaration)) {\n sourceCode += child.toString();\n }\n }\n \n // Get the source code of the class without imports and pass it to generateJavaDoc\n // String classSourceCode = cu.toString();\n generateJavaDoc(sourceCode);\n }\n }\n }\n \n private static void generateJavaDoc(String classSourceCode) throws Exception {\n log.info(\"generating javadoc for \\n\" + classSourceCode);\n\n // Prepare the request\n HttpClient client = HttpClient.newHttpClient();\n \n // Prepare the body\n JSONObject messageBody = new JSONObject();\n messageBody.put(\"model\", \"gpt-3.5-turbo\");\n messageBody.put(\"messages\", List.of(\n Map.of(\"role\", \"system\", \"content\", \"You are a javadoc documentation writer reading the source code, and outputting javadoc only.\"),\n Map.of(\"role\", \"user\", \"content\", classSourceCode)\n ));\n \n // Create HTTP request\n HttpRequest request = HttpRequest.newBuilder()\n .uri(new URI(\"https://api.openai.com/v1/chat/completions\"))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + \"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\")\n .POST(HttpRequest.BodyPublishers.ofString(messageBody.toString(), StandardCharsets.UTF_8))\n .build();\n \n // Send the request\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n \n // Process the response\n JSONObject jsonResponse = new JSONObject(response.body());\n String javaDoc = jsonResponse.getJSONArray(\"choices\").getJSONObject(0).getString(\"message\").getJSONObject(\"content\");\n log.info(javaDoc); // or add it to the class or take other actions\n \n System.exit(0);\n }\n \n}");
messagesArray.put(sampleUserMessage);
JSONObject sampleAssistantMessage = new JSONObject();
sampleAssistantMessage.put("role", "assistant");
sampleAssistantMessage.put("content",
"/**\n * The JavaDocAnalyzer class is responsible for analyzing Java source code and generating Javadoc for classes\n * and interfaces that are missing it.\n */");
messagesArray.put(sampleAssistantMessage);
JSONObject messageBody = new JSONObject();
messageBody.put("messages", messagesArray);
messageBody.put("stop", new JSONArray(List.of("public class", "public interface"))); // stop it passed code start to be outputted
return messageBody;
}
private String extractJavadoc(String input) {
int startIndex = input.indexOf("/**");
int endIndex = input.indexOf("*/");
if (startIndex != -1 && endIndex != -1) {
return input.substring(startIndex + 3, endIndex);
} else {
return null;
}
}
}
Privacy and cost considerations
Your code will be partially sent to OpenAI as part of the completion requests. Please note however, that OpenAI policy
- excludes such data from training
- keeps the data ownership with you for both input (code) and output (docs)
As for the costs of this, refer to this pricing sheet. As a rule of tumb, 1K token is approximately 100 lines of code or docs.

Putting it all together
You can run the above code with the included shell bundler as
export OPENAI_API_KEY==your-openapi-key
./docWriter.sh --srcDir=./src/test/java
This will iterate one file at a time your source code, and add the class level javadoc. For the full documentation and all options refer to the README on GitHub. Dont forget to proof read the documentation, to avoid the mistakes and hallucinations, that a statistical LLM will inevitably make.
Happy documenting!