Writing a protoc plugin in Java
Published 2025-09-05 on Farid Zakaria's Blog
Know thy enemy.
–
Sun TzuAnyone who’s used Protocol Bufffers
We use Protocol Buffers heavily at $DAYJOB$ and it’s becoming increasingly a large pain point, most notably due to challenges with coercing multiple versions in a dependency graph.
Recently, a team wanted to augment the generated Java code protoc (Protobuf compiler) emits. I was aware that the compiler had a “plugin” architecture but had never looked deeper into it.
Let’s explore writing a Protocol Buffer plugin, in Java and for the Java generated code. 🤓
If you’d like to see the end result check out github.com/fzakaria/protoc-plugin-example
Turns out that plugins are simple in that they operate solely over standard input & output and unsurprisingly marshal protobuf over them.
A plugin is just a program which reads a
CodeGeneratorRequest
protocol buffer from standard input and then writes aCodeGeneratorResponse
protocol buffer to standard output. [ref]
The request & response protos are described in plugin.proto.
+------------------+ CodeGeneratorRequest (stdin) +------------------+
| | -------------------------------------------> | |
| protoc | | Your Plugin |
| (Compiler) | <------------------------------------------- | (e.g., in Java) |
| | CodeGeneratorResponse (stdout) | |
+------------------+ +------------------+
|
| (protoc then writes files
| to disk based on plugin's response)
V
+------------------+
| |
| Generated |
| Code Files |
| |
+------------------+
Here is a dumb plugin that emits a fixed class to demonstrate.
public static void main(String[] args) throws Exception {
CodeGeneratorRequest request = CodeGeneratorRequest.parseFrom(System.in);
CodeGeneratorResponse response = CodeGeneratorResponse.newBuilder()
.addFile(
File.newBuilder().setContent("""
// Generated by the plugin
public class Dummy {
public String hello() {
return "Hello from Dummy";
}
}
""")
.setName("Dummy.java")
.build()
)
.build();
response.writeTo(System.out);
}
We can run this and see that the expected file is produced.
> protoc example.proto --plugin=protoc-gen-dumb \
--dumb_out=./generated
> cat generated/Dummy.java
// Generated by the plugin
public class Dummy {
public String hello() {
return "Hello from Dummy";
}
}
Let’s now look at an example in example.proto
.
syntax = "proto3";
option java_package = "com.example.protobuf";
option java_multiple_files = true;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
repeated string phone_number = 4;
Address home_address = 5;
}
message Address {
string street = 1;
string city = 2;
string state = 3;
string zip_code = 4;
}
You can generate the traditional Java code for this using protoc
which by default includes the capability to output Java.
> protoc --java_out=./generated example.proto
> tree generated
generated
└── com
└── example
└── protobuf
└── tutorial
├── Address.java
├── AddressOrBuilder.java
├── Example.java
├── Person.java
└── PersonOrBuilder.java
Nothing out of the ordinary here, we are merely baselining our knowledge. 👌
How can I now modify this code?
If you audit the generated code you will see comments that contain protoc_insertion_point
such as:
@@protoc_insertion_point(message_implements:Person)
> rg "@@protoc_insertion" generated
generated/com/example/protobuf/tutorial/Person.java
13: // @@protoc_insertion_point(message_implements:Person)
417: // @@protoc_insertion_point(builder_implements:Person)
1035: // @@protoc_insertion_point(builder_scope:Person)
1038: // @@protoc_insertion_point(class_scope:Person)
Insertion points are markers within the generated source that allow other plugins to include additional content.
We have to modify our File
that we include in the response to specify the insertion point and instead of a new file being created, the contents of files will be merged. ✨
Our example plugin would like to add the hello()
function to every message type described in the proto file.
We do this by setting the appropriate insertion point which we found from auditing the original generated code. In this particular example, we want to add our new funciton to the Class definition and pick class_scope
as our insertion point.
List<File> generatedFiles = protos.stream()
.flatMap(p -> p.getMessageTypes().stream())
.map(m -> {
final FileDescriptor fd = m.getFile();
String javaPackage = fd.getOptions().getJavaPackage();
final String fileName = javaPackage.replace(".", "/") + "/" + m.getName() + ".java";
return File.newBuilder().setContent("""
// Generated by the plugin
public String hello() {
return "Hello from " + this.getClass().getSimpleName();
}
\s""")
.setName(fileName)
.setInsertionPoint(String.format("class_scope:%s", m.getName()))
.build();
}).toList();
We now run both the Java generator alongside our custom plugin.
We can audit the generated source and we see that our new method is now included! 🔥
Note: The plugin must be listed after java_out
as the order matters on the command-line.
> protoc example.proto --java_out=./generated \
--plugin=protoc-gen-example \
--example_out=./generated
> rg "hello" generated/ -B 1
generated/com/example/protobuf/tutorial/Person.java
1038- // Generated by the plugin
1039: public String hello() {
generated/com/example/protobuf/tutorial/Address.java
862- // Generated by the plugin
863: public String hello() {
While we are limited by the insertion points previously defined in the open-source implementation of the Java protobuf generator, it does provide a convenient way to augment the the generated files.
We can also include additional source files that may wrap the original files for cases where the insertion points may not suffice.
Improve this page @ ffbef24
The content for this site is
CC-BY-SA.