Skip to main content

java-generator

Design of the ejected Redocly `java` client generator. Read it, and update it, before changing generators/java/.

Source facts

Repository
Redocly/redocly-cli
Last source activity
October 27, 2026 at 14:52
Detected SKILL.md language
English
Stars
1,543
Forks
235

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
java-generator
description
Design of the ejected Redocly `java` client generator. Read it, and update it, before changing generators/java/.
# The `java` generator — its skill This file is the DESIGN of your ejected `java` generator (`generators/java/`): **to change the generator, edit this skill first, then make the code match it** — a diff to `generators/java/` that has no covering sentence here is incomplete. ## What it emits One self-contained `<Stem>.java`: a `public final class` named after the output stem in PascalCase (`client.ts` → `Client.java`, `openapi.client.ts` → `OpenapiClient.java`), because Java puts one public class per file and names the file after it. The class nests the model records, the enums, the union interfaces, the embedded runtime, and one typed method per operation. Java ≥ 17 (records, sealed interfaces, `instanceof` patterns), HTTP over `java.net.http.HttpClient`, JSON over the runtime's own reader/writer — zero dependencies. The package derives from the API title the way the PHP namespace does: lowercase words run together (`Cafe Orders API` → `package cafeordersapi;`), because a class in the default package cannot be imported from any named package. A title that cannot be a package as is follows Oracle's package-naming convention — a keyword, or `java`, which the JVM keeps for the JDK, gets a trailing underscore (`java_`, `class_`); a leading digit gets a leading underscore (`_3dprintingapi`); a title with no usable letters becomes `api` — and the run warns with the package it used, since the reader cannot see it in the title. The generator takes no options; a different package is a change to `javaPackageName` in an ejected copy. ## Design decisions that must hold - **JSON is hand-written in the runtime.** Java has no standard JSON library, and a dependency on one would break the zero-dependency story the other SDKs keep. `Json.parse` reads into a plain graph — `Map<String, Object>` (insertion ordered), `List<Object>`, `String`, `Long` for integral numbers, `Double` for the rest, `Boolean`, `null` — and `Json.write` serializes that graph plus the generated types: a `JsonValue` by its `toJson()`, a `WireValue` by its wire literal, a date by its ISO-8601 text. No streaming parse: a body is read whole, then parsed. - **Models are records** implementing the runtime's `JsonValue`. Every component is a reference type (`Long`, not `long`), so an optional or nullable value is simply `null` and a response that omits a required field hydrates instead of crashing. Hydration is compile-time generated per record — `static X fromJson(Object json)` reads each wire name through the runtime's null-tolerant readers (`asString`, `asLong`, `asList(…, item -> …)`, `Order.fromJson`, `Status.fromWire`, `parsePet`), and `toJson()` returns a `Map<String, Object>` of the components (nulls skipped for optionals) that `Json.write` serializes recursively. No reflection. `omit` schemas hydrate through their base record. - **A `writeOnly` property is optional** even when required: requests and responses share one record, and a response never carries it. - **Enums** are Java enums implementing `WireValue`: members in SCREAMING_SNAKE (the toolkit's `enumValues` names, unique per enum) carrying the wire literal — a `String` or a `Long` — with `wire()` and `fromWire(…)`, which returns `null` for `null` and throws `IllegalArgumentException` for an unknown value. Only string and integer enums get a type; other scalars stay their wire type. - **Discriminated unions** whose mapping targets are all records become a `sealed interface X extends JsonValue` that each member record `implements`, so a consumer narrows with `instanceof` patterns; `parseX(Object json)` dispatches on the discriminator and throws `IllegalArgumentException` for an unknown value, since a sealed type has no raw member. `permits` is implied: the members sit in the same compilation unit. A union with a target that has no record of its own is typed `Object`, and its `parseX` returns the raw map for an unknown value. Undiscriminated unions, inline objects, and intersections that do not flatten are `Object`. **allOf** is flattened. - **Types:** `String`/`Long`/`Double`/`Boolean`, `List<T>`, `Map<String, T>`, record and enum names, `Object` for everything untyped. Under `dateType: Date`, `date-time` is `OffsetDateTime` and `date` is `LocalDate`; hydration goes through `asOffsetDateTime` / `asLocalDate`, and the writer formats a date-time with `ISO_OFFSET_DATE_TIME` so the seconds never drop out (`OffsetDateTime.toString()` omits them when zero). - **Reserved type names get a `Model` suffix.** Everything nests in one class, so a schema named like a runtime type (`Config`, `Envelope`) or a JDK type the runtime references (`Thread`, `List`) would shadow it: `naming.ts` reads those names off the runtime source and renames the schema (`ConfigModel`). A schema named like the enclosing class itself cannot nest in it at all, and an enclosing class named like a runtime type (an `--output` of `config.ts`) would make that name mean the class, so both runs fail with the fix (another `--output` name). - **Naming:** types PascalCase, components and methods camelCase via `identifierFor(..., RESERVED_WORDS.java)`; a reserved word gets a trailing underscore (`class_`), `+1`/`-1` become `plus1`/`minus1`, and a digit-leading name gets `_`. Method names are unique across the client (`getUser`, `getUser2`), never one of `Object`'s methods (`getClass` becomes `getClass2`), and each name books its `WithHeaders`, `Pages`, and `Items` variants, so a later operation named like one of them moves aside too. - **Hand-built comments go through `printer.doc`**, never a raw `/** … */` line: a server description or a discriminator value may carry `*/` or `\u`, which end or break a Java comment. `printer.doc` writes `*/` as `*&#47;` and every backslash as `\u005c`, the escape of a backslash, which javac turns back into one without starting another escape — so the Javadoc reads exactly as the description does. - **Every path parameter is its own argument, so their names share one namespace** with the locals the method declares itself (`body`, `params`, `op`, `query`, `url`, …). Build them with `uniqueIdentifiers(..., { taken: … })`: a parameter named `body` becomes `body2`, and the wire name is untouched. Query parameters travel in a `<Op>Params` class — one nullable public field per parameter with a fluent setter of the same name (`new ListOrdersParams().limit(50L)`) — because Java has no named arguments; `null` for the whole class sends none. An array parameter repeats the key per element. - **Method shape:** path parameters first, the body next (`Object` for an untyped multipart or a binary operation), then `<Op>Params` when the operation has query parameters. A JSON success returns its type, a non-JSON success (PDF, image, octet stream) returns `byte[]`, a bodiless success is `void`, and an SSE operation returns `Iterable<ServerSentEvent>`. - **The operation literal is built where it is used** — `Operation op = new Operation(id, method, path, security, pagination)` at the top of each method — never a static table of every operation: a single static initializer of a large description (GitHub: ~1000 operations) exceeds the JVM's 64 KB method size. - **Errors:** unchecked exceptions ARE the error mode. `ApiException` (status, url, decoded body) for a non-2xx response, `ApiTimeoutException` when an attempt exceeds the configured timeout, `UncheckedIOException` for a transport failure; `errorMode` does not change the output (the generator declares `errorModes: ['throw']`, so `result` fails fast). - **Config** is a fluent class: `serverUrl`, `httpClient` (an HTTP/1.1 client by default — the JDK default's h2c upgrade attempt confuses servers that do not speak HTTP/2), `header(name, value)` for every request (a `Content-Type` here wins over the description's, matched case-insensitively, so the header is never sent twice), `timeout` per attempt, `retry` (`Retry`: `retries` extra attempts, default 0; `delay`; `strategy` fixed or exponential; `noJitter`; `retryOn` replacing the default predicate), `middleware` (`onRequest(HttpRequest.Builder)` before each attempt, `onResponse` in reverse order), `idempotencyKey` supplier for POST/PATCH, and `auth` (`Auth`: `bearer`, `basic`, `apiKey` per scheme, each a value or a `Supplier` resolved per request). - **Response headers:** an operation that DECLARES success-response headers gains a `<op>WithHeaders()` variant returning `Envelope<T>` (`data`, `headers` — coerced to `Long`/`Double`/`Boolean`/`String` under camelCase keys, absent or unparsable values omitted — and `status`); a bodiless operation returns `Envelope<Void>`. Operations without declared headers get no variant. - **Pagination:** a paginated operation gains `<op>Pages()` and `<op>Items()`, both `Iterable<T>` over the runtime's `iterPages`, hydrated per page or item through `mapIterable` / `flattenItems`. Each `iterator()` starts a fresh request sequence. - **Servers:** when the description declares servers, a `Servers` class is emitted with one static method per server; server VARIABLES become `String` arguments, and the description's defaults live in the method's doc comment (Java has no default arguments). The client's baked default stays `servers[0]` with variable defaults substituted. - **Stream bodies pass through.** For an untyped `multipart/form-data` operation or a binary operation the body is `Object` and the runtime sorts it out (`multipartBody` / `binaryBody`): an `InputStream` is sent chunked, in exactly one attempt, whatever the retry policy says, because a consumed stream cannot be replayed; a `byte[]` or `String` is sent raw and keeps the retry policy; a map or a record of an untyped multipart operation is encoded with `toMultipart`; any other value of a binary operation is JSON-encoded. The caller's `Content-Type` (a `Config` header) wins over the description's or the multipart boundary's. - **Parity surface:** auth OR-alternatives, retries with `Retry-After` + full-jitter backoff, per-attempt timeouts, idempotency keys, middleware, pagination, SSE (`iterSse` with `Last-Event-ID` reconnect and the server's `retry` hint; auth resolves inside the opener, so each reconnect asks a token supplier again), multipart, and the `X-Redocly-Client` header. - **The runtime is hand-written in `runtime/Runtime.java`** in this folder, an `abstract class Runtime` that compiles on its own (`javac --release 17 -Xlint:all -Werror`) and is embedded at prepare time. Inline, the generator copies its class body into the generated class; under `--runtime module` it writes the same source as `<Stem>Runtime.java`, a `public abstract class` the client `extends`, so every runtime name resolves unqualified in both modes and the two cannot drift. Package-private members stay reachable because both classes share one package. - Authored ONLY with the neutral toolkit — the dogfooding guard fails otherwise. - **It documents itself.** With `client.docs` (or `--docs`), the `docs` hook writes `<stem>.java.md`: the security schemes, then one section per operation with its parameters, body, response type, and behavior notes. The call snippets come from this generator's own `sample` hook, so the page can only show the syntax of the SDK beside it, and the layout comes from `renderReferencePage` in the authoring toolkit — reachable from an ejected copy through `@redocly/client-generator`. ## The modify loop 1. Edit this skill: state the new behavior or decision. 2. Make `generators/java/` match it. 3. Run `redocly generate-client` and inspect the `git diff` of the generated output — generated files are never hand-edited. Newer built-in versions merge in with `redocly eject-generator java --update`.
View on GitHub