# Nexus Client Code Generator

> The Nexus Client Code Generator turns one schema into typed models, runtime validators, and Nexus Service definitions for Go, Java, Python, and TypeScript.

A [Nexus Service](/nexus/services) is a contract meant to be shared across team boundaries.
Those teams often work in different languages, so the same request and response types get hand-written once per SDK.
Hand-written copies drift: a field is required on one side and optional on the other, a bound is enforced by the caller but not the handler.

The **Nexus Client Code Generator** removes those copies.
You describe your types and [Nexus Operations](/nexus/operations) once in a definition file, and the generator emits the equivalent library code for Go, Java, Python, and TypeScript.
The generator is a command-line tool named `nexgen`, distributed from the [temporalio/nex-gen](https://github.com/temporalio/nex-gen) repository.

> **⚠️ Caution:**
>
> `nexgen` is pre-release software, currently at version 0.2.1.
> The supported schema subset, command-line options, and emitted code may change incompatibly before a stable release.
> It is not yet published to any package registry, so you build it from source as described in [Install the generator](#install-the-generator).
>

## What the generator produces

For every type in your definition file, the generator emits three things per language.

- **A typed model.** An idiomatic struct, class, interface, or dataclass, with doc comments carried over from the schema.
- **A shared runtime validator.** One validator per type, used when a value is parsed off the wire and again when it is serialized onto the wire, so a payload cannot enter or leave your service in a shape the contract forbids.
- **A [Nexus Service Contract](/glossary#nexus-service-contract) definition.** The generated Service and Operation declarations you register on a Worker and call from a caller Workflow.

Constraint failures do not surface one at a time.
They aggregate into a single native error listing every violation, each naming the offending field and the bound it broke.
A handler maps that error to a `BAD_REQUEST` [Nexus error](/nexus/error-handling), so a malformed request tells the caller everything that was wrong with it in one response.

The supported schema subset is deliberately strict.
Anything ambiguous, or anything that cannot be expressed identically in all four languages, is rejected when you run the generator, with a diagnostic explaining how to express it instead.
The generator prefers to fail loudly at generation time over emitting code that behaves differently in one language than another.

## Supported languages

`nexgen` generates Go, Java, Python, and TypeScript.

## Definition files

Types are modeled with [JSON Schema 2020-12](https://json-schema.org/draft/2020-12).
A definition file comes in two flavors.

**Pure JSON Schema.** The root of the document is itself a type, and reusable types live under `$defs`.
Use this when you only need data models shared across languages, with no Service or Operation declarations.

**Nexus document.** Add a root `nexusrpc: "1.0.0"` marker to enable a `services` section.
The root becomes an envelope: Services and their Operations sit at the top level, and your types live under `$defs`.

The examples on this page use `samples/schemas/chat.nexusrpc.yaml` from the repository, abbreviated here:

```yaml
nexusrpc: '1.0.0'
$schema: https://json-schema.org/draft/2020-12/schema
services:
  ChatService:
    fqn: example.chat.v1.ChatService
    description: Send messages and look up rooms.
    operations:
      sendMessage:
        description: Post a message to a room.
        input: { $ref: '#/$defs/SendMessageInput' }
        output: { $ref: '#/$defs/SendMessageOutput' }
      getRoom:
        description: Look up a room by id.
        input:
          type: object
          additionalProperties: false
          properties:
            roomId: { type: string }
          required: [roomId]
        output: { $ref: '#/$defs/Room' }
      ping:
        description: Liveness probe.
$defs:
  SendMessageInput:
    type: object
    additionalProperties: false
    properties:
      roomId: { type: string }
      message: { $ref: '#/$defs/Message' }
    required: [roomId, message]
  SendMessageOutput:
    type: object
    additionalProperties: false
    properties:
      messageId: { type: string }
    required: [messageId]
```

`fqn` is the wire name of the Service, the name callers reference when executing an Operation.

An Operation's `input` and `output` are each optional.
The `ping` Operation above declares neither, which generates an Operation that takes and returns nothing.
When present, each must be an object type, so that a field can be added later without breaking the wire format.

The repository holds four example definitions under [`samples/schemas/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas):
[`chat.nexusrpc.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/chat.nexusrpc.yaml),
the feature-diverse [`showcase.nexusrpc.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/showcase.nexusrpc.yaml),
the pure-schema [`temporal.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/temporal.yaml),
and a multi-file closure under [`kb/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas/kb) showing how types split across files resolve through `$ref`.
The `kb/` closure starts at [`kb.nexusrpc.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/kb/kb.nexusrpc.yaml) and pulls in types from its [`content/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas/kb/content) and [`tree/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas/kb/tree) subdirectories.

## Install the generator

Build the `nexgen` binary from source with a Rust toolchain:

```bash
git clone https://github.com/temporalio/nex-gen.git
cd nex-gen
cargo build --release
```

The binary lands at `target/release/nexgen`.
Confirm it works and check which targets your build supports:

```bash
./target/release/nexgen --version
./target/release/nexgen --help
```

## Generate code

Every language uses the same shape: `nexgen <language> <input>... --output <dir>`.
Inputs are positional and may be files or directories, so you can pass a whole multi-file schema closure. Some languages have extra flags.

> **📝 Note:**
>
> The output directory name becomes the generated package or module name.
> Name it after your domain, such as `chat`, not after the language.
> Pointing `--output` at a directory named `go` produces `package go`, which is not valid Go.
>

### Go

```bash
nexgen go samples/schemas/chat.nexusrpc.yaml --output ./chat
```

Place the output directory inside your Go module.
The package name is the directory name, so the example above generates `package chat` in `./chat/chat.go` alongside `./chat/definitions.go`.

### Java

Java requires `--package-name`, and its last dot-separated segment must match the `--output` directory name:

```bash
nexgen java samples/schemas/chat.nexusrpc.yaml \
  --output ./src/main/java/com/example/chat \
  --package-name com.example.chat
```

If the two disagree, generation stops and tells you how to reconcile them:

```
`--package-name com.example.wrong` must end with the output directory name `chat`,
but its last segment is `wrong`; point `--output` at a directory named `wrong` or
change the package's last segment to `chat`
```

### Python

```bash
nexgen python samples/schemas/chat.nexusrpc.yaml --output ./chat
```

This writes an importable package: `models.py`, `services.py`, and an `__init__.py` that re-exports both.
Generated models are [Pydantic](https://docs.pydantic.dev/) models, so your Worker and Client must use the Pydantic Data Converter described in [Use Pydantic models](/develop/python/data-handling/data-conversion#use-pydantic-models).

### TypeScript

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

TypeScript accepts `--date-time-types` to choose how temporal `format` fields are represented in memory:

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

- `string` (the default) keeps every temporal field as the RFC 3339 string that appears on the wire.
  It has no runtime dependency and round-trips losslessly, but you parse and compare the strings yourself.
- `date` maps `date-time` fields to a JavaScript `Date`.
  This is lossy: a `Date` is a UTC instant, so the original offset is folded away and precision is capped at milliseconds.
- `temporal` maps to the TC39 Temporal API, preserving offset and sub-second precision, and requires the `Temporal` global or a polyfill.

## Use the generated code

The generated Service definition is a normal Nexus Service definition.
You register it on a Worker and call it from a caller Workflow exactly as described in your SDK's Nexus guide.
What differs per language is how the validator gets invoked.

| SDK        | How validation reaches the wire                             | Extra step |
| ---------- | ----------------------------------------------------------- | ---------- |
| Go         | Generated `MarshalJSON` and `UnmarshalJSON` on each model    | None       |
| Java       | Generated Jackson serializer and deserializer on each model  | None       |
| Python     | Pydantic model validation                                    | [Use the Pydantic data converter](/develop/python/data-handling/data-conversion#use-pydantic-models) |
| TypeScript | Generated mapper classes                                     | Call the mapper yourself |

In Go, Java, and Python the validator sits in the serialization hook the Temporal data converter already calls, so validation happens on its own once the models are in use.
TypeScript requires an explicit call, covered in [Validate payloads in TypeScript](#validate-payloads-in-typescript).

### Go

The generated `ChatService` value carries the Service name and one typed Operation reference per Operation.
Register handlers on a Worker:

```go
service := nexus.NewService(chat.ChatService.ServiceName)

sendMessage := nexus.NewSyncOperation(chat.ChatService.SendMessage.Name(),
    func(ctx context.Context, input chat.SendMessageInput, _ nexus.StartOperationOptions) (chat.SendMessageOutput, error) {
        return chat.SendMessageOutput{MessageId: store(input)}, nil
    })

if err := service.Register(sendMessage); err != nil {
    return err
}
w.RegisterNexusService(service)
```

Call it from a caller Workflow, passing the generated Operation reference so the SDK type-checks the request and response:

```go
client := workflow.NewNexusClient("chat-endpoint", chat.ChatService.ServiceName)

var output chat.SendMessageOutput
err := client.ExecuteOperation(
    ctx,
    chat.ChatService.SendMessage,
    chat.SendMessageInput{RoomId: "r1", Message: chat.Message{Kind: "text", Body: "hi"}},
    workflow.NexusOperationOptions{},
).Get(ctx, &output)
```

### Java

The generator emits `ChatService` as an interface annotated with `@Service`, with one `@Operation` method per Operation.
On the handler side, write a separate implementation class that points at the generated interface with `@ServiceImpl`, and return an `OperationHandler` from each `@OperationImpl` method:

```java
@ServiceImpl(service = ChatService.class)
public final class ChatServiceImpl {
    @OperationImpl
    public OperationHandler<SendMessageInput, SendMessageOutput> sendMessage() {
        return OperationHandler.sync((ctx, details, input) -> new SendMessageOutput(store(input)));
    }
}
```

Register it on a Worker with `worker.registerNexusServiceImplementation(new ChatServiceImpl())`.

On the caller side, the same interface works directly as a Workflow stub:

```java
ChatService chat = Workflow.newNexusServiceStub(
        ChatService.class,
        NexusServiceOptions.newBuilder()
                .setEndpoint("chat-endpoint")
                .setOperationOptions(NexusOperationOptions.newBuilder()
                        .setScheduleToCloseTimeout(Duration.ofSeconds(10))
                        .build())
                .build());

SendMessageOutput output = chat.sendMessage(new SendMessageInput("r1", message));
```

### Python

The generator emits `ChatService` as a `@service`-decorated class whose attributes are typed `Operation` declarations.
Bind a handler to it:

```python
@service_handler(service=ChatService)
class ChatServiceHandler:
    @sync_operation
    async def send_message(
        self, ctx: StartOperationContext, input: SendMessageInput
    ) -> SendMessageOutput:
        return SendMessageOutput(messageId=store(input))
```

Pass the handler to your Worker as `nexus_service_handlers=[ChatServiceHandler()]`, then call it from a caller Workflow:

```python
client = workflow.create_nexus_client(service=ChatService, endpoint="chat-endpoint")

output = await client.execute_operation(
    ChatService.send_message,
    SendMessageInput(roomId="r1", message=Message(kind="text", body="hi")),
)
```

Generated Python fields are snake_case with the wire name as an alias.
Construct models with either name, and read them with the snake_case attribute: `SendMessageInput(roomId="r1", ...)` constructs, and `output.message_id` reads.

### TypeScript

The generator emits a `chatService` Service definition plus, for each type, an interface and a companion `<Type>Mapper` class:

```typescript
export const chatService = nexus.service('example.chat.v1.ChatService', {
  sendMessage: nexus.operation<SendMessageInput, SendMessageOutput>({ name: 'SendMessage' }),
  getRoom: nexus.operation<GetRoomInput, Room>({ name: 'GetRoom' }),
  ping: nexus.operation<void, void>({ name: 'Ping' }),
});
```

Register a handler against that definition with `nexus.serviceHandler(chatService, { ... })`, and create a caller with `workflow.createNexusServiceClient({ service: chatService, endpoint: 'chat-endpoint' })`.

#### Validate payloads in TypeScript

> **⚠️ Caution:**
>
> In TypeScript the generated validator only runs when you call the mapper.
> No generated payload converter exists, so nothing calls it for you.
>

Each generated type comes with a mapper exposing two methods.
`fromIntermediate` validates an untrusted plain value and returns the typed model.
`toIntermediate` validates a model and returns its plain wire form.
Call them at both edges of every Operation, on the handler side and the caller side:

```typescript
const handler = nexus.serviceHandler(chatService, {
  async sendMessage(_ctx, input) {
    const request = new SendMessageInputMapper().fromIntermediate(input);
    const output = { messageId: await store(request) };
    return new SendMessageOutputMapper().toIntermediate(output) as SendMessageOutput;
  },
});
```

The cast on the return value is expected: `toIntermediate` returns `unknown`, because its result is a plain wire value rather than the model type the Operation declares.

Skipping the mapper is the failure to watch for, because nothing reports it.
The value handed to your handler is typed as the model, since `nexus.operation<SendMessageInput, SendMessageOutput>` declares it that way, but at runtime it is only whatever was deserialized.
A handler that ignores the mapper compiles, type-checks, and returns correct results for valid payloads, while enforcing none of the constraints in your schema.

When a payload does violate the contract, `fromIntermediate` throws a `ValidationError` carrying every violation at once:

```
ValidationError: 2 validation error(s): roomId: required; message.body: expected string
```

The error also exposes a `violations` array of `{ path, reason }` objects, so a handler can convert it into a `BAD_REQUEST` Nexus error with the full list intact.

## Schema defaults

A `default` in your schema is applied when reading, and is never written back to the wire.
The field stays optional in the generated model, and each language exposes the default differently.

- **Go** and **Java** generate an accessor: `PriorityOrDefault()` and `getPriorityOrDefault()`.
- **Python** applies the default through Pydantic, so reading the attribute returns it.
- **TypeScript** exports a module-level constant, such as `DEFAULT_PRIORITY`, that you apply yourself with `value.priority ?? DEFAULT_PRIORITY`.

## Supported schema features

The generator implements a curated subset of JSON Schema 2020-12 chosen so that every accepted construct lowers identically into all four languages.

Fully supported: `properties`, `required`, `default`, `minProperties` and `maxProperties`, `dependentRequired`, string and numeric bounds, `items`, `minItems` and `maxItems`, `minContains` and `maxContains`, `allOf`, the recognized nullable pattern `oneOf: [{type: T}, {type: "null"}]`, and the `title`, `description`, and `deprecated` annotations.

Partially supported: `type` (single-string form only), `additionalProperties`, `propertyNames`, `const` and `enum` (scalars only), `format`, `pattern` (a portable RE2-safe subset), `multipleOf`, `contentEncoding`, `uniqueItems`, `contains`, `oneOf` (branches must be separable by a decidable selector), and `$ref` with `$defs` (local files only).

Deliberately rejected, because they have no coherent typed lowering across all four languages: `anyOf`, `not`, `if`/`then`/`else`, `dependentSchemas`, `prefixItems`, `unevaluatedProperties`, `unevaluatedItems`, `contentMediaType`, and `contentSchema`.

For the current per-keyword support table, see the [nex-gen README](https://github.com/temporalio/nex-gen#supported-json-schema-features).

> **💡 Tip:**
> RESOURCES
>
> - [temporalio/nex-gen](https://github.com/temporalio/nex-gen) for the generator, its README, and the example schemas.
> - [Nexus Services](/nexus/services) for the Service contract concept.
> - Nexus feature guides for registering Services and calling Operations:
>   [Go](/develop/go/nexus/feature-guide) |
>   [Java](/develop/java/nexus/feature-guide) |
>   [Python](/develop/python/nexus/feature-guide) |
>   [TypeScript](/develop/typescript/nexus/feature-guide)
>
