Ultra-fast, zero-dependency parser and serializer for Valve KeyValues (VDF / KV1)
KV1 / VDF + KV3 (Source 2 / CS2) • Zero runtime dependencies • Pure TypeScript • Comment & Conditional Preservation • CLI
Get Started »
Valve uses KeyValues for everything: game configuration (gameinfo.gi), materials, SourceMod configs, localization, layouts, and, in Source 2 / Counter-Strike 2, bot behavior trees and data files.
This library is a modern, lightweight, type-safe implementation of both Valve KeyValues formats for Node.js and Bun, with zero runtime dependencies:
- KV1 / VDF (Source 1, SourceMod, gameinfo): full parser + serializer, comment/order-preserving AST, platform conditionals,
#baseincludes, duplicate-key arrays, and schema validation. - KV3 (Source 2 / CS2): text-format parser + serializer (typed values, arrays,
#base-style objects, multi-line strings, flags), verified against real CS2 behavior-tree files.
There is no real "KV2" format (it is a common naming myth), so this package does not claim it. Binary KV3 encodings are not supported yet.
- Zero runtime dependencies - No heavy parser libraries or helper utilities.
- Both KeyValues formats - KV1/VDF and KV3 (Source 2 / CS2), auto-detected by the KV3 header.
- Handles duplicate keys - Group duplicates into arrays (e.g. search paths) OR detailed AST parsing that preserves order and comments.
- Comment-tolerant - Ignores
//,/* */, and KV3<!-- -->comments. - SteamID-safe - Type conversion keeps 64-bit IDs and oversized integers as strings instead of losing precision.
- Conditionals &
#base- Reads platform conditionals like[!$OSX]and resolves#baseincludes. - CLI included -
parse,stringify,formatandget, with KV1/KV3 auto-detection.
npm install keyvalues-tools
pnpm add keyvalues-tools
bun add keyvalues-toolsimport { parse, stringify } from "keyvalues-tools";
const vdf = `
"GameInfo"
{
"game" "Counter-Strike 2"
"title" "Counter-Strike"
}
`;
// Parse into JS Object
const obj = parse(vdf);
console.log(obj.GameInfo.game); // "Counter-Strike 2"
// Stringify back to VDF
const serialized = stringify(obj);
console.log(serialized);By default, later duplicate keys overwrite earlier ones. Set { duplicates: "array" } to group them:
import { parse } from "keyvalues-tools";
const searchPaths = `
"SearchPaths"
{
"Game" "csgo"
"Game" "core"
}
`;
const obj = parse(searchPaths, { duplicates: "array" });
console.log(obj.SearchPaths.Game); // [ "csgo", "core" ]For tools that need to inspect platform conditionals, edit values, or reconstruct the original VDF exactly with all comments:
import { parseDetailed, stringifyDetailed } from "keyvalues-tools";
const vdf = `
// This is a header comment
"addons"
{
"metamod" "1.12.0" [!$OSX] // Windows/Linux only
}
`;
const ast = parseDetailed(vdf);
console.log(ast[0]);
// {
// key: "addons",
// comment: "This is a header comment",
// value: [
// {
// key: "metamod",
// value: "1.12.0",
// conditional: "!$OSX",
// inlineComment: "Windows/Linux only"
// }
// ]
// }
// Modify values in AST and write back with comments preserved exactly as they were!
const serialized = stringifyDetailed(ast);Convert values that look like numbers or booleans automatically into their JS types:
import { parse } from "keyvalues-tools";
const vdf = `
"Config"
{
"enabled" "true"
"count" "64"
}
`;
const obj = parse(vdf, { convertTypes: true });
console.log(obj.Config.enabled); // true (boolean)
console.log(obj.Config.count); // 64 (number)Filter out unmatched platform keys or blocks when parsing to a standard JS Object:
import { parse } from "keyvalues-tools";
const vdf = `
"addons"
{
"metamod" "1.12.0" [!$OSX]
"metamod" "1.12.0-mac" [$OSX]
}
`;
// Evaluates standard platform tags: 'win32' | 'linux' | 'darwin'
const obj = parse(vdf, { evaluateConditionals: "darwin" });
console.log(obj.addons.metamod); // "1.12.0-mac"Resolve and recursively merge #base files (imports) into the main KeyValues object structure:
import { parse } from "keyvalues-tools";
import { readFileSync } from "fs";
import { join } from "path";
const mainVdf = `
#base "common.vdf"
"Config"
{
"name" "Main Config"
}
`;
const obj = parse(mainVdf, {
resolveBase: (basePath) => {
// Return base file string or pre-parsed KVObject
return readFileSync(join(__dirname, basePath), "utf-8");
}
});Validate a parsed VDF object against a schema structure (similar to Zod/schema validation):
import { parse, KVValidator } from "keyvalues-tools";
const config = parse(vdfString);
const schema = {
GameInfo: {
required: true,
type: {
game: { type: "string", required: true },
maxplayers: { type: "number", required: true },
lan: { type: "boolean", required: false }
}
}
};
const errors = KVValidator.validate(config, schema);
if (errors.length > 0) {
console.error("Validation failed:", errors);
}Parse and serialize the KV3 text format used by Source 2 games. Input is auto-detected from the <!-- kv3 ... --> header via isKV3.
import { parseKV3, stringifyKV3, isKV3, get } from "keyvalues-tools";
const kv3 = `<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:generic:version{7412167c-06e9-4698-aff2-e63eb59037e7} -->
{
difficulty = "hard"
aim_max_duration = 0.9
botId = 76561198012345678 // kept as a string, no precision loss
weapons = [ "ak47", "awp", ] // trailing comma allowed
icon = resource:"materials/x.vmat"
}`;
const data = parseKV3(kv3);
console.log(get(data, "weapons")); // ["ak47", "awp"]
console.log(typeof data.botId); // "string" (SteamID-safe)
const text = stringifyKV3(data); // back to KV3 with the standard headerVerified against real CS2 bot behavior-tree files. Binary KV3 encodings are not supported.
keyvalues-tools parse gameinfo.gi gameinfo.json --convert-types # VDF or KV3 -> JSON (auto-detected)
keyvalues-tools stringify data.json data.vdf # JSON -> VDF (KV1)
keyvalues-tools format addoninfo.txt # normalize / pretty-print (comments preserved)
keyvalues-tools format addoninfo.txt --check # CI: exit 1 if not already formatted
keyvalues-tools get gameinfo.gi GameInfo.FileSystem.SearchPaths # read a value by dot path
keyvalues-tools validate gameinfo.gi schema.json # validate against a schema
keyvalues-tools parse paths.vdf --raw # keep literal Windows pathsWithout an output path, results print to stdout. Also runnable via npx keyvalues-tools.
| Member | Description |
|---|---|
parse(text, options?) |
KV1/VDF to a JS object. Options: duplicates, convertTypes, evaluateConditionals, resolveBase, escapeSequences |
parseDetailed(text, options?) |
KV1 to an ordered KVNode[] AST preserving comments, order, duplicates, conditionals |
stringify(obj, options?) / stringifyDetailed(nodes, options?) |
JS object / AST back to KV1 text. Options: indent, forceQuotes, escapeSequences |
parseKV3(text, options?) |
KV3 (Source 2 / CS2) text to a JS value. { preserveFlags: true } keeps resource:/subclass: flags as KV3Flagged |
parseKV3Detailed(text) / stringifyKV3Detailed(nodes, options?) |
KV3 comment/flag/order-preserving KV3Node[] AST for format-aware edits (object-rooted docs) |
stringifyKV3(value, options?) |
JS value to KV3 text (with the standard header) |
isKV3(text) |
Detect KV3 vs KV1 from the header |
get(obj, "a.b.c") / set(obj, "a.b.c", value) |
Read (KV1 or KV3) or write a nested value by dot path |
diff(a, b) / merge(base, override) |
Compare two objects ({ added, removed, changed }) or deep-merge them (override wins) |
parseFile(path, options?) / parseKV3File(path) |
Read a file and parse it; parseFile auto-resolves #base relative to the file |
KVValidator.validate(obj, schema) |
Validate a parsed object against a schema, returns ValidationError[] |
Windows paths: VDF processes
\n,\tescapes by default, which mangles literal paths like"c:\newmaps". Pass{ escapeSequences: false }(CLI--raw) to keep backslashes literal.Errors report
line L, column Cfor quick debugging of malformed files.
All types (KVObject, KVValue, KVNode, KVDiff, ParseOptions, StringifyOptions, KV3Value, KV3Object, KV3Node, KV3ParseOptions, SchemaObject, ...) are exported.
Self-contained, one topic each (see examples/README.md):
bun run examples/parse.ts # VDF/KV1 -> JS object, type conversion
bun run examples/duplicates-ast.ts # duplicate keys as arrays + comment-preserving AST
bun run examples/conditionals-base.ts # platform conditionals + #base includes
bun run examples/validate.ts # schema validation
bun run examples/edit.ts # get / set / diff + literal Windows paths
bun run examples/kv3.ts # KeyValues3 (Source 2 / CS2)bun test # unit tests incl. KV1, KV3, the official VDC example, and a committed real-shaped fixture
bun run fetch-fixtures # optional: download real KV3 fixtures into test/fixtures/real (gitignored)
bun run lint
bun run type-checkPlease check CONTRIBUTING.md for details.
Distributed under the MIT License. See LICENSE for more information.