# Outputs

> How the output type validates handler returns and shapes JSON / LLM responses.

The handler's return type defines the shape of the command's output. Mark a struct
`#[fireargs::output]` for the serde and schemars derives, camelCase keys and `None`
fields left out. There's no CLI meta for outputs — outputs aren't CLI inputs, so
clap has no per-field surface to configure. The type is pure data shape.

```rust
#[fireargs::input]
struct GreetInput {
    name: String,
}

#[fireargs::output]
struct GreetOutput {
    greeting: String,
    iterations: u32,
}

fn greet() -> fireargs::Command<GreetInput, GreetOutput> {
    fireargs::command().handler(async |input: GreetInput, _| {
        let greeting = format!("hello {}", input.name);
        Ok(GreetOutput { greeting, iterations: 1 })
    })
}
```

Any `Serialize + JsonSchema` type works: `Vec<String>` writes a bare JSON
array under `--json`, a scalar writes a scalar, and `()` writes `null`.
Prefer a struct when the output may grow fields later — and when the CLI is
exposed to MCP clients, whose `outputSchema` must be an object.

## Where the output type is used

1. **Validation** — the handler's return value is serialized with serde and
   validated against the type's JSON Schema on every invocation, so validation
   keys (`range`, `length`, `regex`, …) hold on output too. A mismatch fails the run with `fireargs::OutputError`.
2. **`--json` serialization** — when `--json` is set, the serialized value
   is written to stdout as JSON indented by two spaces. See
   [JSON mode](/rust/json-mode/).
3. **Human rendering** — without `--json`, the value goes to the command's
   render when it has one. See [Commands](/rust/commands/).
4. **`--llms` manifest** — the type's JSON Schema, generated by schemars for
   serialization, is surfaced as the tool's `outputSchema`. See
   [LLMs mode](/rust/llms-mode/).

## Optional fields

An `Option<T>` field of a `#[fireargs::output]` struct is left out of the `--json`
output while `None`, and the `outputSchema` does not require it — the same wire
shape as a TypeScript twin's `.optional()` field. Any other field is always present,
so the `outputSchema` marks it required.

```rust
#[fireargs::output]
struct Page {
    items: Vec<String>,
    total: u32, // always written, required in outputSchema
    next_cursor: Option<String>, // "nextCursor", left out while None
}
```

`#[fireargs(...)]` on an output field takes only `flatten` and the validation keys
(`range`, `length`, …). A hand-derived `Serialize + JsonSchema` struct works as well;
add `#[serde(skip_serializing_if = "Option::is_none")]` to its `Option` fields for
the same wire shape.

## Serialization

serde does the work, so the wire form is each type's `Serialize`, and the
schema is its `JsonSchema`:

| Type                         | On the wire                          | In `outputSchema`                     |
| ---------------------------- | ------------------------------------ | ------------------------------------- |
| integers / floats            | number                               | `integer` / `number`, with a `format` |
| `PathBuf`                    | string                               | `string`                              |
| `HashSet<T>` / `BTreeSet<T>` | array                                | `array` with `uniqueItems: true`      |
| `HashMap` / `BTreeMap`       | object                               | `object` with `additionalProperties`  |
| `std::time::Duration`        | `{"secs": 1, "nanos": 500000000}`    | `object`                              |
| `Vec<u8>`                    | array of numbers                     | `array` of `integer`                  |
| a unit enum                  | the variant name, after `rename_all` | `string` with `enum`                  |
| an enum with data            | serde's representation (tagging)     | `oneOf` the variants                  |
| a nested struct              | object                               | the struct's schema, inlined          |

Types from other crates carry a `JsonSchema` impl behind a schemars feature
— `chrono04`, `uuid1`, `url2`, `rust_decimal1` and others — and serialize as
those crates' serde impls do. A type without `JsonSchema` does not compile
as an output. Convert it to a JSON-native type in the handler.

fireargs deliberately stays plain JSON, so the wire is consumable by any LLM,
shell, or non-fireargs client.

## Dates round-trip

With schemars' `chrono04` feature, `chrono::DateTime<Utc>` serializes as an
RFC 3339 string, deserializes from the same string, and its JSON Schema says
so with `format: date-time`. A date field needs no workaround in either the
input or the output struct.

## What flows to `--llms`

Per property, the JSON Schema in the manifest carries:

- `type`, `format`, `pattern` and bounds — derived from the Rust type and
  the field's schemars attributes.
- `description` — from the field's doc comment.
- `enum` — from a unit enum.
- `required` — every field, except `Option<T>` fields left out while `None`, and
  fields `#[serde(skip_serializing_if)]` leaves out.

Subschemas are inlined, so a nested struct shows in place rather than behind
a `$ref`; the root schema's Rust type name (`title`) is dropped. There's no
CLI-side metadata on outputs, so every other key schemars emits passes
through.

## Why no meta for outputs?

Clap has no output abstraction. CLI users see whatever the render prints (or what
`--json` serializes); there's nothing for fireargs to "wire" beyond running the
validation. Keeping outputs as plain serde types matches clap's surface and avoids
inventing concepts the runtime can't honor.
