nexgen 0.2.7

Generate language-specific Nexus operation bindings from WIT
# nexgen

Generate code for Go, Java, Python, and TypeScript from **NexusRPC definition
files** describing your services and the types they exchange. Each type becomes a
typed model with a runtime validator, wired to the **Nexus service bindings** —
one contract, shared across every language. Types are modeled with **JSON Schema
2020-12**.

> [!WARNING]
> Under active development. The supported JSON Schema subset, CLI options, and
> emitted code may change incompatibly before a stable release.

## What it does

Point `nexgen` at a definition file and it emits, per language, a typed model for
every type in your schema plus one **shared validator** behind it. The same
validator runs on both sides of the wire — when a value is parsed and when it is
serialized — so a payload can never enter or leave your service in a shape the
contract forbids.

Constraint failures don't throw one-at-a-time. They aggregate into a single
native error that carries every violation at once, with each reason naming the
concrete bound and the offending value. A Nexus handler maps that error straight
to `BAD_REQUEST`.

The supported subset is deliberately strict. Anything ambiguous, or anything
that can't be lowered cleanly and identically into all four languages, is
**rejected at generation time** with a fix-it diagnostic that tells you how to
express it instead. The generator would rather reject loudly than emit something
subtly wrong in one language.

## Installation

Download the archive for your platform from the
[latest GitHub release](https://github.com/temporalio/nexgen/releases/latest):

| Platform | Archive target |
| --- | --- |
| Linux (x86-64) | `x86_64-unknown-linux-gnu` |
| Linux (Arm64) | `aarch64-unknown-linux-gnu` |
| macOS (Intel) | `x86_64-apple-darwin` |
| macOS (Apple silicon) | `aarch64-apple-darwin` |
| Windows (x86-64) | `x86_64-pc-windows-msvc` |
| Windows (Arm64) | `aarch64-pc-windows-msvc` |

Extract the archive, then move `nexgen` (`nexgen.exe` on Windows) to a directory
on your `PATH`.

## SDK requirements

Generated code requires at least these Temporal SDK and Nexus SDK versions:

| Language | Temporal SDK | Nexus SDK |
| --- | --- | --- |
| Go | `go.temporal.io/sdk` v1.48.0 | `github.com/nexus-rpc/sdk-go` v0.7.0 |
| Java | `io.temporal:temporal-sdk` 1.39.0 | `io.nexusrpc:nexus-sdk` 0.6.0-alpha |
| Python | `temporalio` 1.32.0 | `nexus-rpc` 1.4.0 |
| TypeScript | `@temporalio/*` 1.23.0 | `nexus-rpc` 0.0.3 |

Keep all TypeScript `@temporalio/*` packages on the same version. Python's
`temporalio` package installs its compatible `nexus-rpc` version transitively;
declare `nexus-rpc` directly only if your dependency tooling requires it.

Generated Java source uses JSpecify's `@NullMarked` and `@Nullable`
annotations. Add JSpecify 1.0.0 to the compile classpath; it is not needed at
runtime. For Gradle:

```groovy
dependencies {
    compileOnly 'org.jspecify:jspecify:1.0.0'
    testCompileOnly 'org.jspecify:jspecify:1.0.0'
}
```

For Maven, use a `provided` dependency:

```xml
<dependency>
  <groupId>org.jspecify</groupId>
  <artifactId>jspecify</artifactId>
  <version>1.0.0</version>
  <scope>provided</scope>
</dependency>
```

## Usage

```
nexgen <lang> [OPTIONS] <INPUT>...

Arguments:
  <lang>       Target language: go | java | python | typescript  (ts = typescript)
  <INPUT>...   One or more JSON Schema definition files or directories
               (.json / .yaml / .yml)

Options:
      --output <DIR>              Directory to write generated code into
      --package-name <PKG>        Java only (required): base package for the
                                  generated types, e.g. com.example.chat. Its last
                                  dot-separated segment must match the --output
                                  directory's name.
      --date-time-types <REPR>    TypeScript only: in-memory type for materialized
                                  temporal `format` fields (date-time/date/time/
                                  duration) — string | date | temporal [default: string]
  -h, --help                      Print help
  -V, --version                   Print version
```

Generate TypeScript bindings from one schema:

```bash
nexgen ts samples/schemas/showcase.nexusrpc.yaml --output ./gen
```

The `--output` directory is created if missing and written into if it already
exists — nothing there is deleted, so hand-written files can live alongside the
generated ones. Generated files are overwritten in place, which also means a
file from an earlier run whose definition has since been renamed or removed
stays behind until you delete it.

Generate Go, TypeScript, and Python:

```bash
for lang in go typescript python; do
  nexgen "$lang" samples/schemas/showcase.nexusrpc.yaml --output "./gen/$lang"
done
```

Java requires `--package-name`, whose last dot-separated segment must match the
`--output` directory's name (here both end in `showcase`):

```bash
nexgen java samples/schemas/showcase.nexusrpc.yaml \
  --output ./gen/com/example/showcase --package-name com.example.showcase
```

Choose how TypeScript represents temporal `format` fields (date-time, date, time,
duration) in memory:

```bash
nexgen ts samples/schemas/temporal.yaml --output ./gen --date-time-types temporal
```

- **`string`** (default) — every temporal field stays an RFC 3339 / ISO 8601
  string, exactly as it appears on the wire. No runtime dependency, runs on any
  Node or browser, and round-trips losslessly (the original offset and sub-second
  precision are preserved). The cost: you parse and compare the strings yourself,
  and the type system won't distinguish a timestamp from any other `string`.
- **`date`** — `date-time` fields become a native JS `Date`; `date`, `time`, and
  `duration` stay strings, because a `Date` can't represent a date-only,
  time-only, or duration value. Convenient for instant math via `.getTime()`, but
  **lossy**: a `Date` is a UTC instant, so the original offset is folded away
  (`2021-06-15T12:30:45.123456+02:00` → `2021-06-15T10:30:45.123Z`) and precision
  is capped at milliseconds. Reach for it only when you already work in `Date`
  and don't care about the offset.
- **`temporal`** — full-fidelity typed values from the TC39 Temporal API:
  `date-time` → `Temporal.ZonedDateTime`, `date` → `Temporal.PlainDate`,
  `duration` → `Temporal.Duration` (`time` stays a string). Preserves offset and
  sub-second precision and gives you real calendar/duration arithmetic, at the
  cost of requiring the `Temporal` global — present in recent runtimes, otherwise
  a polyfill.

See what the output looks like without generating it yourself: the checked-in
results live under `samples/{go,typescript,python,java}/`.

## Definition files

A definition file comes in two flavors.

**Pure JSON Schema** — the root *is* a type. Reusable types live in `$defs` and
are referenced with `$ref`. Good for a single model or a small closure of types:

```yaml
$schema: https://json-schema.org/draft/2020-12/schema
title: Room
type: object
additionalProperties: false
properties:
  roomId: { type: string }
  members:
    type: array
    items: { type: string }
required: [roomId]
```

**Nexus document** — opt in with a root `nexusrpc: "1.0.0"` marker, which enables
a `services` section. Now the root is an envelope: services and operations live
at the top, and your types live in `$defs`:

```yaml
nexusrpc: "1.0.0"
$schema: https://json-schema.org/draft/2020-12/schema
services:
  ChatService:
    fqn: example.chat.v1.ChatService
    operations:
      sendMessage:
        input: { $ref: "#/$defs/SendMessageInput" }
        output: { $ref: "#/$defs/SendMessageOutput" }
$defs:
  SendMessageInput:
    type: object
    additionalProperties: false
    properties:
      roomId: { type: string }
      message: { type: string }
    required: [roomId, message]
  SendMessageOutput:
    type: object
    properties:
      messageId: { type: string }
    required: [messageId]
```

An operation's `input` and `output` are each optional. When present, each must be
an object type (this keeps schemas forward-compatible — a scalar today can't grow
fields later). An omitted side simply means no input, or no output, for that
direction.

The [`samples/schemas/`](samples/schemas/) directory holds example definitions:
the pure-schema [`temporal.yaml`](samples/schemas/temporal.yaml), the Nexus
document [`chat.nexusrpc.yaml`](samples/schemas/chat.nexusrpc.yaml), the
feature-diverse [`showcase.nexusrpc.yaml`](samples/schemas/showcase.nexusrpc.yaml),
and the multi-file closure under [`kb/`](samples/schemas/kb/).

## Naming & overrides

Emitted names are derived **deterministically** from your schema. The generator
never mangles or auto-disambiguates: if two types or members would collide in a
target language, generation **fails at load time** with a fix-it rather than
silently renaming one. The same input always produces byte-identical output.

When the derived name isn't what you want — or to resolve a collision — use the
per-language `x-<lang>-*` extension keywords. They attach to services,
operations, types, and properties:

```yaml
nexusrpc: "1.0.0"
services:
  ChatService:
    x-go-name: Chat              # the Go package is already `chat` — avoid stutter
    fqn: example.chat.v1.ChatService
    operations:
      getRoomByUrl:
        x-go-name: GetRoomByURL  # keep the `URL` initialism uppercase in Go
        input: { $ref: "#/$defs/GetRoomRequest" }
        output: { $ref: "#/$defs/Room" }
$defs:
  Message:
    type: object
    properties:
      from:
        type: string
        x-py-name: sender        # `from` is a Python keyword — it can't be a field
      state:
        enum: ["open", "n/a"]
        # `n/a` isn't a valid identifier, so name the Go/Java constants explicitly
        x-go-enum-names: { "open": Open, "n/a": Unavailable }
        x-java-enum-names: { "open": OPEN, "n/a": UNAVAILABLE }
      schemaVersion:
        const: 2
        x-go-const-name: SchemaVersionV2   # a bare `2` has no usable identifier
        x-java-const-name: SCHEMA_VERSION_V2
```

`x-<lang>-name` sets the emitted identifier — one keyword per target: `x-go-name`,
`x-ts-name`, `x-py-name` (note: the token is `py`, not `python`), and
`x-java-name`. For the languages that emit *named* constants, `x-go-const-name` /
`x-java-const-name` name the identifier for a `const`, and `x-go-enum-names` /
`x-java-enum-names` name each `enum` member (keyed by its wire value). TypeScript
and Python have no equivalent, because there a constant or enum member *is* its
literal value.

## Known cross-language divergences

Three edge cases do not yet behave identically in every target:

- **Negative zero:** TypeScript serializes JSON `-0` as `0`. Go, Java, and
  Python preserve the sign.
- **A very large fractional integer:** the JSON number
  `4503599627370496.5` is rounded before validation by Python and TypeScript,
  so they accept it as an integer. Go and Java retain the original `.5` and
  reject it.
- **Nanosecond timestamps in Python:** Python timestamps hold only six
  fractional digits, so a value such as `.123456789` is re-emitted as
  `.123456`. Go, Java, and TypeScript preserve all nine digits.

The conformance tests pin these differences so that any change is noticed.

## Supported JSON Schema features

Support levels: **Full** (works as specified), **Partial** (a documented subset),
**No** (deliberately rejected with a fix-it), **Planned** (accepted or deferred,
not yet lowered).

### Core & objects

| Feature | Support | Notes |
|---|---|---|
| `properties` | Full | Typed struct fields. |
| `required` | Full | Presence enforced at runtime. |
| `type` | Partial | Single-string form only; the array form is rejected. |
| `default` | Full | Scalar; off-the-wire, materialized on read, never echoed. |
| `additionalProperties` | Partial | Open (default), closed (`false`), or typed map (typed value). |
| `minProperties` / `maxProperties` | Full | Runtime member count. |
| `propertyNames` | Partial | Only on a map-shaped object. |
| `dependentRequired` | Full | Runtime conditional-requirement assertion. |

### Strings & numbers

| Feature | Support | Notes |
|---|---|---|
| `minLength` / `maxLength` | Full | Runtime code-point count. |
| `minimum` / `maximum` | Full | Runtime comparison. |
| `exclusiveMinimum` / `exclusiveMaximum` | Full | Runtime comparison. |
| `const` | Partial | Scalar values only; composite deferred. |
| `enum` | Partial | Scalar values only; composite deferred. |
| `format` | Partial | Curated portable subset, each asserted at runtime. |
| `pattern` | Partial | Portable RE2-safe subset only. |
| `multipleOf` | Partial | Positive integer divisor only. |
| `contentEncoding` | Partial | `base64` / `base64url` only, materialized to native bytes. |

### Arrays

| Feature | Support | Notes |
|---|---|---|
| `items` | Full | Homogeneous list (`type: array` + single `items`). |
| `minItems` / `maxItems` | Full | Runtime element count. |
| `minContains` / `maxContains` | Full | Runtime match-count bounds. |
| `uniqueItems` | Partial | Scalar element types only. |
| `contains` | Partial | Scalar matcher over scalar elements only. |

### Composition & nullability

| Feature | Support | Notes |
|---|---|---|
| `allOf` | Full | Load-time merge into one schema; only contradictory/unrepresentable branches reject. |
| nullability | Full | The recognized `oneOf: [{type: T}, {type: "null"}]` pattern. |
| `oneOf` | Partial | Branches must be separable by a decidable selector (≥2 branches). |
| `$ref` / `$defs` | Partial | Named targets in local files only; no `$id`, no remote. |
| `anyOf` | No | No coherent typed lowering in the strict subset. |
| `not` | No | Negation has no coherent typed lowering. |
| `if` / `then` / `else` | No | Conditional shape has no coherent lowering. |
| `dependentSchemas` | No | Branches object shape on a runtime condition. |
| `prefixItems` | No | Positional tuples don't lower coherently across targets. |
| `unevaluatedProperties` / `unevaluatedItems` | No | No coherent lowering in the strict subset. |
| `contentMediaType` / `contentSchema` | No | No coherent lowering / nowhere to emit. |

### Annotations & Nexus

| Feature | Support | Notes |
|---|---|---|
| `title` / `description` | Full | Emitted as doc-comment summary/body. |
| `deprecated` | Full | Native deprecation marker + doc note. |
| `$comment` | Full | Accepted, silently dropped. |
| `services` / `operations` | Full | First-class (Nexus documents). |
| `examples` | Planned | Accepted and ignored; pending the doc-comment feature. |
| `readOnly` | Planned | Deferred; needs request/response split-types. |
| `writeOnly` | Planned | Deferred; a single type can't hold a per-direction field. |
| `patternProperties` | Planned | Rejected in v1; a narrow single-pattern form is tracked. |

## Examples

Sample inputs live under [`samples/schemas/`](samples/schemas/) and their
generated output — in every language — under
[`samples/`](samples/)`{go,typescript,python,java}/`. Two are worth starting with:

- [`showcase.nexusrpc.yaml`]samples/schemas/showcase.nexusrpc.yaml — a tour
  exercising most of the supported subset in one file.
- [`kb/`]samples/schemas/kb/ — a multi-file Nexus closure, showing how types
  split across files resolve through `$ref`.