Skip to content

Repository files navigation

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

Under development; not yet published to package registries. For now, build the nexgen binary from source:

cargo build --release   # produces target/release/nexgen

Or run it straight from the repo with cargo run -- <args> (the examples below use this form). Prebuilt binaries will be distributed via GitHub releases.

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

Generate TypeScript bindings from one schema:

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

Generate Go, TypeScript, and Python:

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):

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:

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.
  • datedate-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:002021-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-timeTemporal.ZonedDateTime, dateTemporal.PlainDate, durationTemporal.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:

$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:

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/ directory holds example definitions: the pure-schema temporal.yaml, the Nexus document chat.nexusrpc.yaml, the feature-diverse showcase.nexusrpc.yaml, and the multi-file closure under 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:

nexusrpc: "1.0.0"
services:
  ChatService:
    x-go-name: Chat              # the Go package is already `chat` — avoid stutter
    fqn: example.chat.v1.ChatService
    operations:
      getRoomById:
        x-go-name: GetRoomByID   # keep the `ID` 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.

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/ and their generated output — in every language — under samples/{go,typescript,python,java}/. Two are worth starting with:

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages