sdxc

[ Content & formats ]

@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 and stringify mirror their JSON counterparts, return a Result instead of throwing, and are separate exports, so a reader that only parses bundles only the parser.

Installation

npm add @sdxc/yaml

Both functions answer a Result from @sdxc/result, which provides isFailure and installs alongside this package.

Usage

Read a document

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;

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

The value is unknown. A date reads as a string, since the core schema has no timestamp type, and a source with no nodes (empty, blank or only comments) reads as null.

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;

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

Output is block style and ends in a line break; pass { indent: 4 } to widen each level.

Use both under one name

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

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

The namespace import tree-shakes the same as named imports.

API

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

Parses YAML source into the value it describes, or null for a source with no nodes. Source outside the supported subset fails with a YAMLParseError.

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

Writes a value as a block-style YAML document ending in a line break. Values follow JSON.stringify (toJSON is used, an undefined object entry is dropped, an undefined array entry becomes null), except that NaN and the infinities are written as .nan, .inf and -.inf. Strings are written plain when that reads back unchanged, as a literal block when they span lines, and double-quoted otherwise; empty collections are written {} and []. A circular structure or a bigint fails with a YAMLStringifyError.

YAMLParseError

The failure of parse. line is the line parsing stopped on, counting from 1, and the message ends with at line <n>.

YAMLStringifyError

The failure of stringify. path points at the offending value, as items.0.parent, and is empty at the document root; the message ends with at <path> when the path is set.

Types

StringifyOptions

  • indent: spaces each nesting level adds. Defaults to 2.

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:
Every other scalar resolves to a stringlastUpdated: 2026-08-02

These fail to parse: anchors, aliases and merge keys; tags; explicit keys (? ); directives; duplicate keys in one mapping; quoted values spanning lines; multi-document sources; tab indentation; and a plain scalar opening on a reserved character (@, `, %, ,, ], }).

Pattern: Validating parsed YAML with a schema

parse answers unknown. Pass the value to any Standard Schema validator to get a typed result:

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

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

export async function readConfig<T>(source: string, schema: StandardSchemaV1<unknown, T>) {
	let parsed = parse(source);
	if (isFailure(parsed)) return { issues: [{ message: parsed.error.message }] };

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

Pattern: Editing a document

Read, change a value, and write it back. Every value in the subset survives the round trip; comments and the original layout are replaced by the serializer's own:

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 });
}

Pattern: Writing text that needs quoting

stringify picks the notation that reads back as the same value, so strings that look like other types, contain indicators, or span lines need no manual escaping:

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

let written = stringify({
	title: "a: b",
	flag: "true",
	body: "Line one\nLine two\n",
	publishedAt: new Date("2026-08-02T00:00:00Z"),
});
if (isFailure(written)) throw written.error;

written.data;
// title: "a: b"
// flag: "true"
// body: |
//   Line one
//   Line two
// publishedAt: 2026-08-02T00:00:00.000Z

parse(written.data); // flag stays "true", body keeps its line breaks, the date is a string

Pattern: Reporting where a file went wrong

Both errors say where the failure is: line in the source being read, path in the value being written.

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

export function check(filename: string, source: string) {
	let result = parse(source);
	if (isFailure(result)) {
		let line = source.split("\n")[result.error.line - 1];
		return `${filename}: ${result.error.message}\n  ${line}`;
	}
	return null;
}

let written = stringify({ items: [{ parent: 1n }] });
if (isFailure(written)) written.error.path; // "items.0.parent"

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.