yaml
yaml is a full YAML parser and stringifier for JavaScript, written by Eemeli Aro. At the surface it works like JSON: parse(str) gives you plain objects, stringify(value) gives you YAML text. Underneath there are two more layers most libraries do not have: a Document API that keeps comments, blank lines, and anchors intact while you edit values, and a raw lexer/parser/composer for building tools like linters and formatters. It supports both YAML 1.1 and 1.2, passes the official yaml-test-suite, has zero dependencies, and runs in Node and browsers. It is what Prettier and a large slice of the config-tooling ecosystem parse YAML with.
The most spec-correct YAML library in JavaScript and the only good choice when you must round-trip files with comments intact. If all you do is read a trusted config once, js-yaml is lighter and equally fine.
Use it if
- You need to edit a YAML file and write it back without destroying comments and formatting: parseDocument plus setIn is the only mainstream JS option that does this well
- You care about spec correctness, like multi-document streams, anchors, merge keys, or the differences between YAML 1.1 and 1.2, because this library passes the full yaml-test-suite
- You are building YAML tooling (linter, language server, formatter) and need access to tokens and the concrete syntax tree, not just the parsed values
- You want zero dependencies and dual CJS/ESM support so it drops into any build without module-format drama
- You only ever call one parse on trusted config at startup and size matters: js-yaml is the older standard, and at 31.3 KB gzipped this library is heavier than what a single load-a-config call needs
- You expect YAML 1.1 behavior by default: this library defaults to YAML 1.2, so yes/no parse as strings not booleans and merge keys (<<) are ignored unless you opt in, which regularly surprises people porting from PyYAML or old js-yaml
- You want a schema validation story: yaml gives you data, not validation; you still need zod, ajv, or similar on top, and if config validation is the whole job a TOML or JSON pipeline may be simpler
- The project is effectively one maintainer; releases are steady and quality is high, but there is no company or team behind it if that matters for your risk policy
Setup reality
npm install yaml and you are done: zero dependencies, bundled TypeScript types, works from require() and import both. The friction is conceptual rather than mechanical. The three API layers (parse/stringify, Document, Lexer/Parser) confuse newcomers who land on the AST docs when they just wanted parse. The YAML 1.2 default bites anyone with legacy files full of yes/no booleans or sexagesimal numbers. And the included types currently target TypeScript 5.9, so older TS setups may need skipLibCheck. A v3 is published on the next dist-tag, so double-check which major your lockfile resolves before reading docs.
Patterns
Parse and stringify like JSONparse-and-stringify
import { parse, stringify } from 'yaml'
const data = parse('a: 1\nlist:\n - x\n - y\n')
// { a: 1, list: ['x', 'y'] }
const text = stringify({ number: 3, block: 'two\nlines\n' })
// number: 3
// block: |
// two
// linesparse throws on invalid YAML. If you want errors collected instead of thrown, use parseDocument and check doc.errors.
Load a YAML config fileread-config-file
import fs from 'node:fs'
import { parse } from 'yaml'
const config = parse(fs.readFileSync('./config.yml', 'utf8'))
console.log(config.server.port)There are no file helpers by design; the library also runs in browsers, so reading the file stays on your side.
Control indentation and line wrappingstringify-formatting
import { stringify } from 'yaml'
stringify(data, {
indent: 4,
lineWidth: 0, // disable line folding entirely
defaultStringType: 'QUOTE_DOUBLE'
})The default lineWidth of 80 folds long strings across lines, which surprises people diffing generated files; 0 turns folding off.
Edit a file without losing commentspreserve-comments
import fs from 'node:fs'
import { parseDocument } from 'yaml'
const doc = parseDocument(fs.readFileSync('app.yml', 'utf8'))
doc.setIn(['server', 'port'], 8080)
fs.writeFileSync('app.yml', String(doc))This is the feature that justifies the library: comments, blank lines, and anchors survive the round trip. Plain parse/stringify discards them.
Parse a multi-document streammulti-document-streams
import { parseAllDocuments } from 'yaml'
const src = 'a: 1\n---\nb: 2\n---\nc: 3\n'
for (const doc of parseAllDocuments(src)) {
console.log(doc.toJS())
}Kubernetes manifests and CI files often concatenate documents with ---; parse only reads a single document and errors on the rest.
Collect errors instead of throwingcollect-errors
import { parseDocument } from 'yaml'
const doc = parseDocument(userInput)
if (doc.errors.length > 0) {
for (const err of doc.errors) {
console.error(err.code, err.message, err.pos)
}
} else {
const value = doc.toJS()
}Each error carries a code and character positions, which is what you want for showing squiggles in an editor or validating user uploads.
Handle YAML 1.1 files and merge keysyaml-1-1-merge-keys
import { parse } from 'yaml'
// full 1.1 semantics: yes/no become booleans, << merges work
parse(src, { version: '1.1' })
// stay on 1.2 but honor merge keys
parse(src, { merge: true })The default is YAML 1.2, so yes/no are plain strings and << is ignored. This is the top porting gotcha from PyYAML or docker-compose-era files.
Walk and transform the ASTvisit-transform-nodes
import { parseDocument, visit } from 'yaml'
const doc = parseDocument(src)
visit(doc, {
Scalar(key, node) {
if (typeof node.value === 'string') {
node.value = node.value.trim()
}
}
})
console.log(String(doc))Return visit.REMOVE from a visitor to delete a node, or a replacement node to swap it; this is how linters and codemods are built on this library.
Restrict parsing to JSON-compatible valuesjson-compatible-strict-mode
import { parse } from 'yaml'
parse(src, { schema: 'json' })
// only JSON types allowed; 0x10, dates, and !!binary all failUseful when the output must survive JSON.stringify unchanged, like config that gets forwarded to another service.
Build a document with comments from scratchbuild-document-programmatically
import { Document } from 'yaml'
const doc = new Document({ name: 'svc', replicas: 2 })
doc.commentBefore = ' Generated file, do not edit'
doc.get('replicas', true).comment = ' scale with care'
console.log(String(doc))
// # Generated file, do not edit
// name: svc
// replicas: 2 # scale with careget(key, true) returns the underlying node instead of the JS value; comments hang off nodes, not off plain values.
Create anchors and aliasesanchors-and-aliases
import { Document } from 'yaml'
const doc = new Document({ base: { retries: 3 }, job: {} })
const alias = doc.createAlias(doc.get('base', true), 'defaults')
doc.setIn(['job', 'config'], alias)
console.log(String(doc))
// base: &defaults
// retries: 3
// job:
// config: *defaultsAliases are references to the same node, so later edits to the anchored node show up everywhere the alias resolves.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| js-yaml | npm | You want the long-standing default with a smaller footprint and only need plain load/dump without comment preservation. |
| gray-matter | npm | Your actual task is parsing YAML front matter out of Markdown files, which it handles end to end. |