Search
Search documentation
Outputs
Updated Oct 23, 2026
.output::<O>() defines the shape of whatever the handler returns. There's
no fireargs meta for outputs — outputs aren't CLI inputs, so clap has no
per-field surface to configure. The type is pure data shape.
use fireargs::{self as f, Command, CommandConfig, Error};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, JsonSchema)]
struct GreetInput {
name: String,
}
#[derive(Serialize, JsonSchema)]
struct Greeting {
greeting: String,
iterations: u32,
}
fn create_greet() -> Command {
f::command(CommandConfig::default())
.input::<GreetInput>()
.output::<Greeting>()
.handler(|input, _| async move {
let greeting = format!("hello {}", input.name);
Ok::<_, Error>(Greeting { 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
- Validation — the handler's return value is serialized with serde and
validated against the type's JSON Schema on every invocation, so
schemars' validation attributes (
range,length,regex, …) hold on output too. A mismatch fails the run withfireargs::OutputError. --jsonserialization — when--jsonis set, the serialized value is written to stdout as JSON indented by two spaces. See JSON mode.- Human rendering — without
--json, the value goes to the command's render when it has one. See Commands. --llmsmanifest — the type's JSON Schema, generated by schemars for serialization, is surfaced as the tool'soutputSchema. See LLMs mode.
Optional fields
An Option<T> field is written as null while None, and the
outputSchema requires it. Add
#[serde(skip_serializing_if = "Option::is_none")] to leave it out of the
--json output instead, and the outputSchema no longer requires it — the
same wire shape as a TypeScript twin's .optional() field. Any other field
is always present, so the outputSchema marks it required.
use schemars::JsonSchema;
use serde::Serialize;
#[derive(Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
struct Page {
items: Vec<String>,
total: u32, // always written, required in outputSchema
#[serde(skip_serializing_if = "Option::is_none")]
next_cursor: Option<String>, // left out while None
}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,patternand 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, exceptOption<T>fields that skip serializing whileNone, 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.
Created with ❤ and Livemark