sdxc

Type to search, or start from one of these:

@sdxc/yaml

YAML reading and writing over a documented subset, shaped after the built-in JSON object

npm add @sdxc/yaml
pnpm add @sdxc/yaml
yarn add @sdxc/yaml
bun add @sdxc/yaml
Installs with
@sdxc/result

YAML reading and writing over a documented subset, shaped after the built-in JSON object.

parse turns YAML text into JavaScript values and stringify writes them back out, named after their JSON counterparts and exported one by one, so importing the reader leaves the serializer out of the bundle. Where JSON throws, both halves return a Result.

The covered subset is block mappings and sequences, plain and quoted scalars, flow collections, block scalars and comments. Anchors, aliases, merge keys, tags and multi-document sources are parse failures, named as such rather than silently misread.

Installation

npm add @sdxc/yaml

Both halves report their outcome as a Result from @sdxc/result, which is where isFailure and isSuccess come from. It installs alongside this package.

Usage

Read A Document

import { isFailure } from "@sdxc/result";
import { parse } from "@sdxc/yaml";

let result = parse("title: API Keys\norder: 3\n");
if (isFailure(result)) throw result.error;

let data = result.data; // { title: "API Keys", order: 3 }

The value is unknown, which is the honest type for text read off a file. A source holding no nodes — empty, blank, or only comments — reads as null.

Read Nested Collections

import { isFailure } from "@sdxc/result";
import { parse } from "@sdxc/yaml";

let result = parse(`
title: API Keys
section:
  title: Team & Settings
  order: 3
tags: [remix, workers]
lastUpdated: 2026-08-02
`);

if (isFailure(result)) throw result.error;

let data = result.data;
// {
//   title: "API Keys",
//   section: { title: "Team & Settings", order: 3 },
//   tags: ["remix", "workers"],
//   lastUpdated: "2026-08-02",
// }

lastUpdated comes back as a string: the core schema has no timestamp type, so a date is text on the way in and text on the way back out.

Write A Value

import { isFailure } from "@sdxc/result";
import { stringify } from "@sdxc/yaml";

let result = stringify({
	title: "API Keys",
	section: { title: "Team & Settings", order: 3 },
	tags: ["remix", "workers"],
});

if (isFailure(result)) throw result.error;

console.log(result.data);
// title: API Keys
// section:
//   title: Team & Settings
//   order: 3
// tags:
//   - remix
//   - workers

The output is block style, ending in a line break. An empty mapping or sequence is written in the flow style, as {} and [].

Reach For Both Under One Name

import * as YAML from "@sdxc/yaml";

let written = YAML.stringify({ title: "Hello" });
let read = YAML.parse("title: Hello\n");

Both import forms shake the same, so reaching for the namespace costs nothing.

API

parse(source: string): Result<unknown, YAMLParseError>

Parses YAML source into the value it describes, answering null for a source holding no nodes. The result is unknown: hand it to a Standard Schema validator to give it a type. Source that falls outside the subset is a YAMLParseError carrying the line it stopped on.

let result = parse("title: Hello\norder: 1\n");
if (isFailure(result)) return;

let data = result.data; // { title: "Hello", order: 1 }

stringify(value: unknown, options?: StringifyOptions): Result<string, YAMLStringifyError>

Writes a value as a YAML document in the block style, ending in a line break.

Values follow JSON.stringify: a toJSON method is used where a value has one, an undefined object entry is dropped, and an undefined array entry becomes null. A function and a symbol are treated the same way. NaN and the infinities, which JSON writes as null, become .nan, .inf and -.inf. A circular structure and a bigint are a YAMLStringifyError.

options.indent is how many spaces each nesting level adds, and defaults to 2.

let result = stringify({ tags: ["remix"] }, { indent: 4 });
if (isFailure(result)) return;

let text = result.data; // "tags:\n    - remix\n"

YAMLParseError

The error in the failure branch of parse. line is the line parsing stopped on, counting from 1, and the message is the reason with that line appended.

YAMLStringifyError

The error in the failure branch of stringify. path points at the offending value, as items.0.parent, and is empty at the document root; the message is the reason with that path appended.

Types

StringifyOptions

interface StringifyOptions {
	indent?: number;
}

The Supported Subset

SupportedExample
Block mappings, nested by space indentationsection: / order: 1
Block sequences of scalars, mappings or sequencestags: / - remix
Plain scalars, folded when written across linestitle: Team & Settings
Single- and double-quoted scalars, with escapestitle: "a: b"
Flow sequences and mappings, which may span linestags: [remix, workers]
Literal and folded block scalars, with chomping and an indent indicatordescription: >-
Comments, whole-line and trailingorder: 1 # first
null (null, ~, empty), booleans, integers, floatslastUpdated:
Everything else scalar-shaped resolves to a stringlastUpdated: 2026-08-02

Reported as a failure rather than guessed at: anchors, aliases and merge keys; tags; explicit keys (? ); directives; duplicate keys in one mapping; quoted values spanning lines; multi-document sources; tab indentation. A plain scalar opening on a character YAML reserves — @, `, %, ,, ], } — is a failure too, as it is in YAML.

Pattern: Validating Parsed YAML With A Schema

parse answers unknown, which is the honest type for text from a file. Pair it with a Standard Schema validator to get a typed value and one failure branch for both steps. Any library implementing the specification works, so the schema can come from whichever one an application already uses:

import type { StandardSchemaV1 } from "@standard-schema/spec";

import { isFailure } from "@sdxc/result";
import { parse } from "@sdxc/yaml";

export function readConfig<T>(source: string, schema: StandardSchemaV1<unknown, T>) {
	let parsed = parse(source);
	if (isFailure(parsed)) return parsed;

	return schema["~standard"].validate(parsed.data);
}

Pattern: Round-Tripping A Document

Reading a document, changing one value, and writing it back stays lossless for everything the subset covers. Comments and the original formatting belong to the text rather than the value, so the document that comes out carries the values it went in with and the serializer's own layout:

import { isFailure } from "@sdxc/result";
import * as YAML from "@sdxc/yaml";

export function bumpOrder(source: string) {
	let parsed = YAML.parse(source);
	if (isFailure(parsed)) return parsed;

	let data = parsed.data as { order: number };
	return YAML.stringify({ ...data, order: data.order + 1 });
}

Writing a document through stringify is what keeps the quoting right: it picks the notation that reads back unchanged, which hand-written text has to get right itself.

Pattern: Diagnosing A Bad File

Both errors carry where the failure was, which is what makes a rejected file fixable without re-reading it. line points at a source line, and path at a value inside the structure being written:

import { isFailure } from "@sdxc/result";
import { parse } from "@sdxc/yaml";

let result = parse(source);

if (isFailure(result)) {
	let { line, message } = result.error;
	console.error(`${filename}:${line} ${message}`);
	console.error(source.split("\n")[line - 1]);
}