Generate web API models and routing metadata from Protocol Buffers.
A single .proto file describes request/response types, HTTP method, path templates, validation annotations, and JSON field naming. The protoc plugins emit:
- Java — POJO classes (
WebpbMessage), Jackson/validation annotations, and Spring@WebpbRequestMappingwiring via an annotation processor. - TypeScript — classes with
webpbMeta(),create(), andfromAlias()for typed HTTP clients.
Proto extensions are defined in lib/proto/src/main/resources/webpb/WebpbExtend.proto.
sample/ is an end-to-end webpb options demo.
| Module | Role |
|---|---|
sample/proto |
Shared .proto files and WebpbOptions.proto (global Java/TS defaults) |
sample/backend |
Spring Boot app; Gradle or Maven; generates Java from proto |
sample/frontend |
Vite + React + TypeScript app; generates TS from proto via webpb generate |
Request messages carry routing metadata in (m_opts).opt. Path segments use {field} or {nested.field} placeholders; mark query fields with (opts).opt = {in_query: true}.
message QueryParamRequest {
option (m_opts).opt = {
method: "GET"
path: "/options/query?tag={tag}"
};
required string tag = 1 [(opts).opt = {in_query: true}];
}Generated types implement WebpbMessage. Add @WebpbRequestMapping on controller methods; the annotation processor fills in @RequestMapping from the request message metadata.
@RestController
public class OptionsController {
@WebpbRequestMapping
public QueryParamResponse queryParam(@Valid QueryParamRequest request) {
// ...
}
@WebpbRequestMapping
public HttpRouteResponse httpRoute(@Valid @RequestBody HttpRouteRequest request) {
// ...
}
}Register WebpbHandlerMethodArgumentResolver and WebpbRequestBodyAdvice in Spring MVC configuration (see WebMvcConfiguration.java).
Generated classes expose webpbMeta() with method and path. The sample HttpService sends JSON requests using that metadata:
this.httpService.request(
QueryParamRequest.create({ tag: "demo" }),
QueryParamResponse,
);Per-project defaults live in a shared WebpbOptions.proto imported by every file. The sample declares Java imports for validation annotations and TS defaults:
option (f_opts).java = {
import: 'jakarta.validation.Valid'
import: 'jakarta.validation.constraints.NotNull'
// ...
};
option (f_opts).ts = {
int64_as_string: false
};Field-level Jackson naming in the sample uses message options and placeholders:
option (m_opts).java = {field_annotation: '@JsonProperty("{{_ALIAS_}}")'};
option (m_opts).ts = {auto_alias: true};Options are attached at file, message, enum, field, or enum-value level. Import webpb/WebpbExtend.proto in every proto file.
| Group | Field | Description |
|---|---|---|
java |
gen_getter |
Generate getters (default true) |
java |
gen_setter |
Generate setters (default true) |
java |
import |
Extra Java imports for generated annotations/types |
java |
annotation |
Class-level annotations on every generated type |
java |
field_annotation |
Field annotations; supports {{_ALIAS_}}, {{_FIELD_NAME_}} |
java |
repeatable_annotation |
Fully qualified annotation types allowed to repeat |
ts |
import |
Extra TypeScript imports |
ts |
int64_as_string |
Serialize int64 as string in JSON |
ts |
auto_alias |
Derive JSON field names from proto field names |
ts |
default_const_enum |
Emit const enums for protobuf enums |
ts |
enum_auto_alias |
Emit secondary runtime alias enum (EnumX / ConstX); default true |
ts |
enum_values_literal |
Emit XValues as numeric literals ([0, 1, 2]) instead of member references; default false |
ts |
enum_by_name |
Emit XByName forward map (name → number); default false |
ts |
enum_by_value |
Emit XByValue reverse map (number → name); default false |
ts |
enum_helpers |
Emit xFromName / xToName helpers when by_name / by_value maps exist; default false |
ts |
enum_emit_mode |
Enum output shape: ts (default), js_dts, or ts_and_js_dts |
| Group | Field | Description |
|---|---|---|
opt |
method |
HTTP method for request messages (e.g. GET, POST) |
opt |
path |
URL template with {field} placeholders |
opt |
context |
Base path prefix |
opt |
extends |
Java superclass name |
opt |
implements |
Java interfaces to implement |
opt |
sub_type |
Subtype discriminator for polymorphic messages |
opt |
sub_values |
Allowed subtype values |
opt |
alias_reserve |
Reserved alias index for child messages; must be greater than this message's max field id. Child alias index is max(ancestor max index, alias_reserve) + field id - 1 |
java |
annotation |
Class-level Java annotations |
java |
field_annotation |
Default field annotations for all fields in the message |
ts |
auto_alias |
Override file-level auto_alias for this message |
| Group | Field | Description |
|---|---|---|
opt |
omitted |
Exclude field from generated API surface |
opt |
in_query |
Bind field to query string (for GET / path templates) |
java |
annotation |
Java field annotations (e.g. @NotNull, @Pattern(...)) |
java |
as_set |
Generate repeated fields as Set<T> instead of List<T> |
java |
as_collection |
Generate repeated fields as Collection<T> instead of List<T> |
ts |
as_string |
Serialize numeric field as string |
ts |
alias |
JSON property name override |
ts |
auto_alias |
Override alias behavior for this field |
Repeated fields default to List<T>. Use Java field options to change the collection type:
repeated string tags = 1 [(opts).java = {as_set: true}];
repeated int32 ids = 2 [(opts).java = {as_collection: true}];| Group | Field | Description |
|---|---|---|
opt |
string_value |
Use string values instead of numeric ordinals |
java |
annotation |
Enum-level Java annotations |
java |
implements |
Java interfaces for the enum |
ts |
default_const_enum |
Override file-level const-enum behavior |
ts |
enum_auto_alias |
Override file-level enum_auto_alias |
ts |
enum_values_literal |
Override file-level enum_values_literal |
ts |
enum_by_name |
Override file-level enum_by_name |
ts |
enum_by_value |
Override file-level enum_by_value |
ts |
enum_helpers |
Override file-level enum_helpers |
ts |
enum_emit_mode |
Override file-level enum_emit_mode |
When default_const_enum is enabled, webpb emits a primary const enum X plus a secondary runtime alias (enum EnumX or const enum ConstX, depending on the primary). That bidirectional alias increases frontend bundle size when bundled with esbuild, SWC, or Rolldown without tsc inlining.
The enum_* options above are opt-in tuning knobs. Set them in (f_opts).ts for all enums in a file, or in (e_opts).ts to override a single enum. Resolution order: enum → file → WebpbOptions.proto.
Recommended preset for smaller frontend bundles:
option (f_opts).ts = {
default_const_enum: true
enum_emit_mode: "js_dts"
enum_auto_alias: false
enum_values_literal: true
enum_by_name: false
};Enable enum_by_name or enum_by_value only on enums that need runtime name lookup (e.g. parsing config strings or logging). Global enum_by_name: true emits every member name as a string key into the bundle.
enum Bar {
option (e_opts).ts = { enum_by_name: true };
// ...
}Example output for Foo (with per-enum enum_by_name: true):
export const enum Foo {
a = 0,
b = 1,
c = 2,
}
export const FooValues: readonly Foo[] = [0, 1, 2];
export const FooByName = {
a: 0,
b: 1,
c: 2,
} as const;
export type FooName = keyof typeof FooByName;
export function fooFromName(name: FooName): Foo {
return FooByName[name];
}When enum_helpers: true, webpb emits narrow lookup functions (xFromName when enum_by_name is on, xToName when enum_by_value is on) as a typed replacement for legacy EnumX[name] / EnumX[code] access.
When enum_values_literal is enabled, Values is typed as readonly X[] so for...of loops do not require as X[] casts. Maps use as const with XName / XByValueKey helper types.
Message fields that reference enums from another proto file use import type { X } instead of import * as XEnum, so the message module does not pull in enum runtime exports (Values, ByName) unless the field needs enum members at runtime (e.g. sub_values in polymorphic messages still use namespace imports).
When set to js_dts (or ts_and_js_dts for migration), each enum is emitted as a separate {EnumName}.d.ts + {EnumName}.js pair. The {Package}.ts file becomes a shim that re-exports types from ./{EnumName} and runtime values from ./{EnumName}.js — it does not inline a second const enum. The .d.ts holds const enum types and declare bindings; the .js holds only runtime values (literal Values, optional maps/helpers). Bundlers can import the .js directly without tsc inlining.
ts_and_js_dts generates the same shim + split files as js_dts (no duplicate inline enum). Use it only as a migration alias; prefer js_dts for new projects.
option (f_opts).ts = {
default_const_enum: true
enum_emit_mode: "js_dts"
enum_auto_alias: false
enum_values_literal: true
};Foo.d.ts:
export const enum Foo {
a = 0,
b = 1,
c = 2,
}
export declare const FooValues: readonly Foo[];Foo.js:
export const FooValues = [0, 1, 2];When the protobuf package name differs from the enum name (e.g. package BarEnum + enum Bar), {Package}.ts is the stable entry for legacy imports:
// BarEnum.ts (shim)
export type { Bar } from "./Bar";
export { BarValues } from "./Bar.js";Enum-only proto files still emit {Package}.ts (the shim). Message fields in the same file use the shim re-export without a separate import. Cross-file references use import type { Bar } from "./BarEnum" (the package shim), not ./Bar.js, so hand-written code and generated messages share one Bar type.
XValues lists every declared numeric value in proto declaration order. It is not a continuous [0..max] range. For example, if members are a = 0, b = 20, c = 23, then XValues is [0, 20, 23]. Use enum_by_name / enum_by_value only when you need runtime name↔value lookup; webpb does not emit min/max or isValid(n) helpers by default.
| From | To |
|---|---|
import { EnumX, X } |
import { X, XByName } or import type { X } |
EnumX[name] |
XByName[name] or xFromName(name) when enum_helpers: true |
EnumX[code] |
xToName(code) or XByValue[code] when enum_by_value: true |
Object.entries(EnumX) |
Object.entries(XByName) |
Set enum_auto_alias: true temporarily (or keep legacy defaults) while migrating call sites, then disable it once imports no longer reference EnumX.
| Group | Field | Description |
|---|---|---|
opt |
value |
String representation when string_value is enabled |
java |
annotation |
Per-value Java annotations |
Published artifacts (Maven Central, group io.github.jinganix.webpb):
| Artifact | Purpose |
|---|---|
webpb-gradle-plugin |
Gradle convention plugins |
webpb-protoc-java |
Java protoc plugin (:all fat jar or platform binary) |
webpb-protoc-ts |
TypeScript protoc plugin (:all fat jar or platform binary) |
webpb-proto |
WebpbExtend.proto and well-known types for -I / protobuf(...) deps |
webpb-runtime |
Java runtime library |
webpb-processor |
Spring @WebpbRequestMapping annotation processor |
webpb (npm) |
TypeScript runtime and webpb generate CLI |
Every .proto file must import webpb extensions:
import "webpb/WebpbExtend.proto";Add webpb-proto as a dependency (Gradle/Maven) or pass -I to a directory that contains the webpb/ folder.
Apply the protobuf Gradle plugin together with a webpb convention plugin:
Java (sample/backend / pom.xml):
plugins {
id("com.google.protobuf") version "0.9.6"
id("io.github.jinganix.webpb.java") version "0.0.33"
}
dependencies {
protobuf("io.github.jinganix.webpb:webpb-proto:0.0.33")
implementation("io.github.jinganix.webpb:webpb-runtime:0.0.33")
annotationProcessor("io.github.jinganix.webpb:webpb-processor:0.0.33")
// your .proto module or files
protobuf(project(":your-proto-module"))
}TypeScript:
plugins {
id("com.google.protobuf") version "0.9.6"
id("io.github.jinganix.webpb.ts") version "0.0.33"
}
dependencies {
protobuf("io.github.jinganix.webpb:webpb-proto:0.0.33")
}Optional configuration:
webpb {
webpbVersion = "0.0.33" // defaults to the Gradle plugin version
protobufVersion = "4.35.1" // com.google.protobuf:protoc version
cleanOutput = false // default true: delete output dir before generation
localPluginPath = "/path/to/webpb-protoc-java" // skip Maven resolution
}The convention plugin configures protoc, registers the webpb or ts plugin from Maven (webpb-protoc-*:all@jar), removes the built-in java generator, and wires generateProto tasks. When developing webpb from source, a binary under plugin/bin/ is picked up automatically.
Manual setup (without the convention plugin) is also supported:
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.35.1"
}
plugins {
id("ts") {
artifact = "io.github.jinganix.webpb:webpb-protoc-ts:0.0.33:all@jar"
}
}
generateProtoTasks {
ofSourceSet("main").forEach {
it.builtins { remove("java") }
it.plugins { id("ts") }
}
}
}Use protobuf-maven-plugin with the published webpb-protoc-java launcher JAR (classifier: all). See sample/backend/pom.xml.
<properties>
<webpb.version>0.0.33</webpb.version>
<protobuf.version>4.35.1</protobuf.version>
</properties>
<dependencies>
<dependency>
<groupId>io.github.jinganix.webpb</groupId>
<artifactId>webpb-proto</artifactId>
<version>${webpb.version}</version>
</dependency>
<dependency>
<groupId>io.github.jinganix.webpb</groupId>
<artifactId>webpb-runtime</artifactId>
<version>${webpb.version}</version>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<protocPlugins>
<protocPlugin>
<id>webpb</id>
<groupId>io.github.jinganix.webpb</groupId>
<artifactId>webpb-protoc-java</artifactId>
<version>${webpb.version}</version>
<classifier>all</classifier>
<mainClass>io.github.jinganix.webpb.java.Main</mainClass>
</protocPlugin>
</protocPlugins>
</configuration>
<executions>
<execution>
<goals>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>Run mvn generate-sources to generate Java under target/generated-sources/protobuf/.
For Spring MVC, add webpb-processor as an annotation processor dependency.
Install the runtime (dev dependency) and generate TypeScript with the bundled CLI:
npm install -D webpbThe webpb CLI resolves protoc and webpb-protoc-ts automatically, includes bundled webpb/WebpbExtend.proto, and writes one .ts file per protobuf package (for example OptionsProto.ts).
npx webpb generate \
-o src/generated \
-I path/to/your/protos \
path/to/your/protos/*.protoCommon options:
| Option | Description |
|---|---|
-o, --out |
Output directory (--ts_out), required |
-I, --include |
Additional proto include path (repeatable) |
--no-webpb-proto |
Skip bundled webpb proto includes |
--protoc |
Path to protoc (default: PATH, then Maven Central download) |
--plugin |
Path to webpb-protoc-ts (default: GitHub Releases download) |
--webpb-version |
webpb release version for plugin download |
--protobuf-version |
protoc version for auto-download (default: bundled, aligned with Gradle) |
Environment variables: PROTOC, WEBPB_PROTOC_TS, WEBPB_VERSION, WEBPB_PROTOBUF_VERSION.
Resolution order for tools:
webpb-protoc-ts: WEBPB_PROTOC_TS → monorepo plugin/bin/ (when developing webpb) → cached GitHub release (~/.cache/webpb/{version}/) → download.
protoc: PROTOC → PATH → cached Maven Central download (~/.cache/webpb/protoc/{version}/).
Bundled proto includes ship in the npm package (node_modules/webpb/proto). Override with --no-webpb-proto when supplying your own -I paths.
Add to package.json:
{
"scripts": {
"proto": "webpb generate -o src/generated -I protos protos/*.proto"
}
}Sample frontend (uses the local webpb package from runtime/ts/build; run npm run build in runtime/ts first when developing from source):
cd sample/frontend
npm install
npm run protoFor manual protoc invocation, download a release binary from GitHub Releases (webpb-protoc-ts-<platform>) or set WEBPB_PROTOC_TS. See protoc CLI below.
webpb plugins are standard protoc plugins. They read CodeGeneratorRequest from stdin and write generated files to --*_out.
Get a plugin binary
- GitHub Releases —
webpb-protoc-java-<platform>/webpb-protoc-ts-<platform> - Maven Central —
io.github.jinganix.webpb:webpb-protoc-java:VERSION:all(run viajava -jaror let Gradle/Maven resolve it) - Build from source (requires Go):
cd plugin && make build
Generate Java (plugin name webpb):
protoc \
-I path/to/webpb-proto-include \
-I path/to/your/protos \
--plugin=protoc-gen-webpb=/path/to/webpb-protoc-java \
--webpb_out=path/to/output \
path/to/your/protos/*.protoGenerate TypeScript (plugin name ts):
protoc \
-I path/to/webpb-proto-include \
-I path/to/your/protos \
--plugin=protoc-gen-ts=/path/to/webpb-protoc-ts \
--ts_out=path/to/output \
path/to/your/protos/*.protoClone the repository, then start backend and frontend in separate terminals.
Gradle:
./gradlew sample:backend:bootRunOn Windows:
./gradlew.bat sample:backend:bootRunGradle applies io.github.jinganix.webpb.java, runs the webpb protoc plugin, and uses the annotation processor to generate Java sources under sample/backend/build/generated/.
Maven:
cd sample/backend
mvn spring-boot:runMaven uses protobuf-maven-plugin with the webpb protoc plugin and webpb-processor as an annotation processor. Generated sources are written under sample/backend/target/generated-sources/protobuf/.
Install Node.js, build the local webpb npm package, then start the frontend:
cd runtime/ts && npm ci && npm run build
cd sample/frontend
npm install
npm startnpm start runs webpb generate, which resolves protoc and webpb-protoc-ts and writes TypeScript to sample/frontend/generated/proto/. See npm / TypeScript for details.
Open http://localhost:4200. The dev server proxies /stores to the backend at http://localhost:8181.
See CONTRIBUTING.md for how to report issues and submit changes.