@cli-schema/zod
v0.2.0
Published
Derives CLI Schema parameter definitions from Zod schemas (https://github.com/cli-schema/cli-schema)
Readme
@cli-schema/zod
Derives CLI Schema parameter definitions from Zod schemas — types, requiredness, defaults, descriptions, and JSON-Schema validation constraints, all read directly off the schema you already wrote for validation.
Part of the cli-schema-js monorepo. Framework-
agnostic: it doesn't know about Commander, yargs, or any other CLI library — it only knows Zod
in, structured parameter data out. @cli-schema/commander
builds on top of it.
Quickstart
npm install @cli-schema/zod zodimport { z } from 'zod'
import { extractSchemaArgs } from '@cli-schema/zod'
const SearchInput = z.object({
index: z.string().describe('Index to search'),
size: z.number().default(10).describe('Number of results'),
})
extractSchemaArgs(SearchInput)
// [
// { schemaKey: 'index', cliFlag: 'index', type: 'string', required: true, description: 'Index to search' },
// { schemaKey: 'size', cliFlag: 'size', type: 'number', required: false, description: 'Number of results', defaultValue: 10 },
// ]That's the core of it: one Zod object schema in, one CLI parameter definition per top-level key out.
Purpose
If your CLI already validates its input with Zod, that schema is a complete, precise
description of every flag the command accepts — type, requiredness, default, description,
enum/range/length constraints. Re-describing all of that by hand for help text, documentation, or
a CLI Schema document is duplicated, drifting work. @cli-schema/zod reads it back out.
It deliberately knows nothing about any particular CLI's own conventions (routing metadata,
custom parsing hints, reserved flag names) — those vary per project, so they're exposed as
extension points (enrich, a reserved list) rather than baked in.
Features
extractSchemaArgs— the main entry point. Walks az.object()'s top-level shape and returns oneSchemaArgDefinitionper key:- Type inference across
string,number,integer(zod'sz.int()),boolean,object,array,enum— including through.optional(),.default(),z.lazy(),z.union()(first member wins),z.record()/z.any()/z.unknown()(→object). - Kebab-case flag names derived from the schema key (
toKebabCase) — handlessnake_case,camelCase, and strips leading underscores (_source→source). - Requiredness and defaults — a field is required only if it's neither optional nor
defaulted;
defaultValueis read straight off.default(...). - Descriptions from
.describe()/.meta({ description }). acceptsArrayFormdetection for theunion(T, array(T))pattern — a common way to let a field accept either a single value or a collection while keeping the type inference on the scalar branch for CLI ergonomics.- An
enrichhook so your own framework can attach extra fields (routing metadata, parse hints, anything) without this package needing to know what they mean.
- Type inference across
validateSchemaArgs— fail-fast collision detection: throws if two schema keys would produce the same CLI flag, or if a flag collides with your CLI's reserved names.buildFlagKeyMap— a bidirectionalcliFlag↔schemaKeymap, for merging parsed CLI flags back into the shape your Zod schema expects.- JSON-Schema-derived enrichment (
zodToJsonSchema,extractEnumValues,extractElementType,extractValidations) — for the detailextractSchemaArgsalone can't carry (enum values, array element types, range/length/regex/email/url constraints), reusing Zod's owntoJSONSchema()rather than re-implementing schema introspection. readMetaField/walkWrapperChain— the low-level primitives the above are built on, exported for CLIs that need their own custom extraction (e.g. afound_inrouting tag, or a named-shape check like "is this schema taggedSortanywhere in its wrapper chain").
Usage
Basic extraction
import { z } from 'zod'
import { extractSchemaArgs } from '@cli-schema/zod'
const args = extractSchemaArgs(z.object({
numShards: z.number().min(1).max(100),
level: z.enum(['low', 'medium', 'high']).default('medium'),
tags: z.array(z.string()).optional(),
}))
args.map((a) => a.cliFlag) // ['num-shards', 'level', 'tags']Attaching framework-specific extras with enrich
Say your CLI routes some fields to an HTTP path/query/body via .meta({ found_in: '...' }).
Rather than this package hardcoding that convention, thread it through enrich:
import { z } from 'zod'
import { extractSchemaArgs, type SchemaArgDefinition } from '@cli-schema/zod'
interface MyExtras {
foundIn?: 'path' | 'query' | 'body'
}
const schema = z.object({
index: z.string().meta({ found_in: 'path' }),
q: z.string().optional().meta({ found_in: 'query' }),
})
const args: Array<SchemaArgDefinition & MyExtras> = extractSchemaArgs(schema, {
enrich: (field, base) => {
const foundIn = (field.meta() as Record<string, unknown> | undefined)?.['found_in']
return foundIn != null ? { foundIn } : {}
},
})Validating for flag collisions
import { validateSchemaArgs } from '@cli-schema/zod'
// throws: "help" collides with a name your CLI already uses for --help
validateSchemaArgs(extractSchemaArgs(z.object({ help: z.string() })), ['help', 'json', 'config-file'])reserved defaults to ['help'] if omitted, since nearly every CLI framework reserves
--help.
Round-tripping flags back to schema keys
import { buildFlagKeyMap } from '@cli-schema/zod'
const map = buildFlagKeyMap(args)
map.toSchemaKey.get('num-shards') // 'numShards'
map.toCliFlag.get('numShards') // 'num-shards'Enum values, array element types, and validation constraints
extractSchemaArgs alone doesn't carry enum values or range/length constraints — those need the
full JSON Schema, which is a separate (and more expensive) step best done once per command:
import { z } from 'zod'
import { extractSchemaArgs, zodToJsonSchema, extractEnumValues, extractElementType, extractValidations } from '@cli-schema/zod'
const schema = z.object({
level: z.enum(['low', 'medium', 'high']),
size: z.number().min(1).max(100),
})
const root = zodToJsonSchema(schema)
const properties = root.properties as Record<string, Record<string, unknown>>
for (const arg of extractSchemaArgs(schema)) {
const node = properties[arg.schemaKey]
console.log(arg.cliFlag, {
enumValues: extractEnumValues(node, root),
elementType: extractElementType(node, root), // only meaningful when arg.type === 'array'
validations: extractValidations(node, root), // e.g. [{ kind: 'range', min: '1', max: '100' }]
})
}extractValidations covers the constraint kinds directly derivable from JSON Schema keywords —
range (minimum/maximum), length (minLength/maxLength), regex (pattern), email
and url (format). Kinds that need domain knowledge beyond what JSON Schema expresses
(timeSpanRange, count, allowed, denied, uriScheme, fileExtensions, existing,
nonExisting, rejectSymbolicLinks — see the spec)
aren't inferred automatically; attach them via the enrich hook if your schema encodes them.
Related packages
@cli-schema/spec— theConstraint/Parametertypes this package's JSON-Schema helpers produce@cli-schema/commander— wires this package's output into a fullCliSchemadocument from a live Commander tree
License
MIT
