Skip to content

Add YAML support to parse()/safeParse() via pluggable decoders #9

@mildronize

Description

@mildronize

Summary

Introduce a small “decode layer” between stdout and validation so parse() and safeParse() can handle YAML in addition to JSON without breaking existing behavior. Callers can opt into YAML explicitly or rely on auto-detection. The same decode path is used by runParse() and safeRunParse().

Motivation

  • Many CLIs emit YAML by default (kubectl, helm, aws with --output yaml).
  • Today parse() assumes JSON, forcing extra conversion steps.
  • A format-agnostic decoder keeps the fluent API unchanged while adding YAML support and future extensibility (TOML, INI, CSV).

Scope

  • Add a tiny decoder interface and registry.
  • Provide built-in JSON and YAML decoders (yaml package).
  • Add optional parse options to parse() and safeParse(), and to runParse() and safeRunParse().
  • Default behavior remains backward compatible: auto-detect with JSON first, then YAML.

Public API changes

Add types:

export type ParseFormat = 'json' | 'yaml' | 'auto';

export interface ParseOptions {
  from?: ParseFormat;                 // default: 'auto'
  allowEmpty?: boolean;               // empty -> {} when true
  yamlMulti?: 'first' | 'all' | 'error'; // multi-doc handling
}

export interface Decoder {
  name: Exclude<ParseFormat, 'auto'>;
  test?(s: string): boolean;
  parse(s: string, opts?: ParseOptions): unknown;
}

Extend ShellOptions:

export interface ShellOptions<Mode extends OutputMode = 'capture'> extends OverridableCommandOptions<Mode> {
  throwMode?: 'simple' | 'raw';
  logger?: ShellLogger;
  decoders?: Decoder[];                // default: [JsonDecoder, YamlDecoder]
  defaultParse?: ParseOptions;         // default: { from: 'auto', yamlMulti: 'first' }
}

Update method signatures (non-breaking; options are optional):

// Fluent
parse<T extends StandardSchemaV1>(schema: T, options?: ParseOptions): Promise<StandardSchemaV1.InferOutput<T>>;
safeParse<T extends StandardSchemaV1>(schema: T, options?: ParseOptions): Promise<ValidationResult<StandardSchemaV1.InferOutput<T>>>;

// Eager
runParse<T extends StandardSchemaV1, Mode extends OutputMode = DefaultMode>(
  cmd: string | string[], schema: T, options?: RunOptions<Mode> & { parse?: ParseOptions }
): Promise<StandardSchemaV1.InferOutput<T>>;

safeRunParse<T extends StandardSchemaV1, Mode extends OutputMode = DefaultMode>(
  cmd: string | string[], schema: T, options?: RunOptions<Mode> & { parse?: ParseOptions }
): Promise<ValidationResult<StandardSchemaV1.InferOutput<T>>>;

Internal changes

  • Add this.decoders and this.defaultParse to Shell.

  • Implement private decode(stdout: string, opts?: ParseOptions): unknown:

    • If from is set, use that decoder.
    • If auto, try JSON first (cheap and strict), then YAML.
    • Support multi-document YAML via yaml.parseAllDocuments.

Built-in decoders

  • JsonDecoder: uses JSON.parse, optional test on leading { or [.
  • YamlDecoder: uses yaml library; supports multi-document (first | all | error).

Usage examples

Explicit YAML:

const $ = createShell().asFluent();
const cfg = await $('kubectl get cm app -o yaml').parse(ConfigSchema, { from: 'yaml' });

Auto-detect:

const info = await $('aws eks describe-cluster --name x --output yaml').parse(ClusterSchema);

Multi-doc:

const docs = await $('helm template mychart').parse(HelmSchema, { from: 'yaml', yamlMulti: 'all' });

Safe path:

const r = await $('cat values.yaml').safeParse(ValuesSchema, { from: 'yaml', allowEmpty: true });

Acceptance criteria

  • parse() and safeParse() successfully validate YAML and JSON outputs using the same schemas.
  • Auto-detection prefers JSON, falls back to YAML; errors are clear when decoding fails.
  • Multi-document YAML behaves according to yamlMulti.
  • No regressions for existing JSON-only users.
  • Documentation updated with examples and guidance.

Risks and notes

  • JSON is valid YAML; ordering the auto-detect as JSON → YAML avoids overly permissive parsing.
  • Users who want deterministic behavior should pass { from: 'yaml' } explicitly.
  • Streaming (live) remains unsupported for parsing; this is already enforced in Fluent mode.

Tasks

  • Add ParseOptions, ParseFormat, and Decoder types.
  • Implement JsonDecoder and YamlDecoder (add yaml dependency).
  • Extend ShellOptions with decoders and defaultParse.
  • Add decode() helper and wire it into parse() / safeParse() (fluent).
  • Wire decode() into runParse() / safeRunParse() (eager).
  • Unit tests: JSON, YAML, auto-detect, empty output, error paths, multi-doc modes.
  • Update README with examples and migration notes.
  • Changelog entry.

Out of scope

  • Additional formats (TOML, INI, CSV).
  • Tagged template escaping or live mode parsing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions