Fireargs is in technical previewShare feedback

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

  1. 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 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.
  3. Human rendering — without --json, the value goes to the command's render when it has one. See Commands.
  4. --llms manifest — the type's JSON Schema, generated by schemars for serialization, is surfaced as the tool's outputSchema. 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:

TypeOn the wireIn outputSchema
integers / floatsnumberinteger / number, with a format
PathBufstringstring
HashSet<T> / BTreeSet<T>arrayarray with uniqueItems: true
HashMap / BTreeMapobjectobject with additionalProperties
std::time::Duration{"secs": 1, "nanos": 500000000}object
Vec<u8>array of numbersarray of integer
a unit enumthe variant name, after rename_allstring with enum
an enum with dataserde's representation (tagging)oneOf the variants
a nested structobjectthe 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 that skip serializing 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.

Created with ❤ and Livemark