Modify JSONC while preserving comments and formatting.
npm install aywsonššš šš ššššššš, ššš?
Modify JSONC while preserving comments and formatting.
``sh`
npm install aywson
`ts`
import {
parse, // parse JSONC to object
modify, // replace fields, delete unlisted
get, // read value at path
set, // write value at path (with optional comment)
remove, // delete field at path
merge, // update fields, keep unlisted
replace, // alias for modify
patch, // alias for merge
rename, // rename a key
move, // move field to new path
getComment, // read comment (above or trailing)
setComment, // add comment above field
removeComment, // remove comment above field
getTrailingComment, // read trailing comment
setTrailingComment, // add trailing comment
removeTrailingComment, // remove trailing comment
sort, // sort object keys
format // format/prettify JSONC
} from "aywson";
Replace fields, delete unlisted. Comments above deleted fields are also deleted, unless they start with **.
`ts
import { modify } from "aywson";
modify('{ / keep this / "a": 1, "b": 2 }', { a: 10 });
// ā '{ / keep this / "a": 10 }' ā comment preserved, b deleted
`
modify uses replace semantics ā fields not in changes are deleted. Comments (both above and trailing) on deleted fields are also deleted, unless they start with **.
Parse a JSONC string into a JavaScript value. Unlike JSON.parse(), this handles comments and trailing commas.
`ts
import { parse } from "aywson";
parse({
// database config
"host": "localhost",
"port": 5432,
});
// ā { host: "localhost", port: 5432 }
// With TypeScript generics
interface Config {
host: string;
port: number;
}
const config = parse
`
Paths can be specified as either:
- String paths: "config.database.host" or "items[0].name" (dot-notation, like the CLI)["config", "database", "host"]
- Array paths: or ["items", 0, "name"]
Both formats work for all path-based operations.
Get a value at a path.
`ts
// Using string path
get('{ "config": { "enabled": true } }', "config.enabled");
// ā true
// Using array path
get('{ "config": { "enabled": true } }', ["config", "enabled"]);
// ā true
`
Check if a path exists.
`ts`
has('{ "foo": "bar" }', "foo"); // ā true (string path)
has('{ "foo": "bar" }', ["foo"]); // ā true (array path)
has('{ "foo": "bar" }', "baz"); // ā false
Set a value at a path, optionally with a comment.
`ts
// Using string path
set('{ "foo": "bar" }', "foo", "baz");
// ā '{ "foo": "baz" }'
// Using array path
set('{ "foo": "bar" }', ["foo"], "baz");
// ā '{ "foo": "baz" }'
// With a comment
set('{ "foo": "bar" }', "foo", "baz", "this is foo");
// ā adds "// this is foo" above the field
// Nested paths work with both formats
set('{ "config": {} }', "config.enabled", true);
// or
set('{ "config": {} }', ["config", "enabled"], true);
`
Remove a field. Comments (both above and trailing) are also removed, unless they start with **.
`ts{
// Using string path
remove(
// this is foo
"foo": "bar",
"baz": 123
},
"foo"
);
// ā '{ "baz": 123 }' ā comment removed too
// Using array path
remove(
{
"foo": "bar", // trailing comment
"baz": 123
},
["foo"]
);
// ā '{ "baz": 123 }' ā trailing comment removed too
// Nested paths
remove(json, "config.database.host");
// or
remove(json, ["config", "database", "host"]);
`
Update/add fields, never delete (unless explicit undefined).
`ts`
merge('{ "a": 1, "b": 2 }', { a: 10 });
// ā '{ "a": 10, "b": 2 }' ā b preserved
Delete fields not in changes (same as modify).
`ts`
replace('{ "a": 1, "b": 2 }', { a: 10 });
// ā '{ "a": 10 }' ā b deleted
Alias for merge. Use undefined to delete.
`ts`
patch('{ "a": 1, "b": 2 }', { a: undefined });
// ā '{ "b": 2 }' ā a explicitly deleted
Rename a key while preserving its value.
`ts
// Using string path
rename('{ "oldName": 123 }', "oldName", "newName");
// ā '{ "newName": 123 }'
// Using array path
rename('{ "oldName": 123 }', ["oldName"], "newName");
// ā '{ "newName": 123 }'
// Nested paths
rename(json, "config.oldKey", "newKey");
// or
rename(json, ["config", "oldKey"], "newKey");
`
Move a field to a different location.
`ts
// Using string paths
move(
'{ "source": { "value": 123 }, "target": {} }',
"source.value",
"target.value"
);
// ā '{ "source": {}, "target": { "value": 123 } }'
// Using array paths
move(
'{ "source": { "value": 123 }, "target": {} }',
["source", "value"],
["target", "value"]
);
// ā '{ "source": {}, "target": { "value": 123 } }'
// Mixed formats also work
move(json, "source.value", ["target", "value"]);
`
Sort object keys alphabetically while preserving comments (both above and trailing) with their respective keys.
`ts{
sort(
// z comment
"z": 1,
// a comment
"a": 2
});
// ā '{ "a": 2, "z": 1 }' with comments preserved
// Trailing comments are also preserved
sort({
"z": 1, // z trailing
"a": 2 // a trailing
});`
// ā '{ "a": 2 // a trailing, "z": 1 // z trailing }'
Path: Specify a path to sort only a nested object (defaults to "" or [] for root).
`ts
// Using string path
sort(json, "config.database"); // Sort only the database object
// Using array path
sort(json, ["config", "database"]); // Sort only the database object
// Root level (both equivalent)
sort(json); // or sort(json, "") or sort(json, [])
`
Options:
- comparator?: (a: string, b: string) => number ā Custom sort function. Defaults to alphabetical.deep?: boolean
- ā Sort nested objects recursively. Defaults to true.
`ts
// Custom sort order (reverse alphabetical)
sort(json, "", { comparator: (a, b) => b.localeCompare(a) });
// Only sort top-level keys (not nested objects)
sort(json, "", { deep: false });
// Sort only a specific nested object, non-recursively
sort(json, "config", { deep: false });
`
``
Format a JSONC document with consistent indentation. Preserves comments while normalizing whitespace.
`ts
import { format } from "aywson";
// Format minified JSON
format('{"foo":"bar","baz":123}');
// ā '{
// "foo": "bar",
// "baz": 123
// }'
// Comments are preserved
format('{ / important / "foo": "bar" }');
// ā '{
// / important /
// "foo": "bar"
// }'
``
Options:
- tabSize?: number ā Number of spaces per indentation level. Defaults to 2.insertSpaces?: boolean
- ā Use spaces instead of tabs. Defaults to true.eol?: string
- ā End of line character. Defaults to '\n'.
`ts
// Use 4 spaces for indentation
format(json, { tabSize: 4 });
// Use tabs instead of spaces
format(json, { insertSpaces: false });
// Use Windows-style line endings
format(json, { eol: "\r\n" });
`
Add or update a comment above a field.
`ts{
// Using string path
setComment(
"enabled": true
},
"enabled",
"controls the feature"
);
// ā adds "// controls the feature" above the field
// Using array path
setComment(json, ["config", "enabled"], "controls the feature");
`
Remove the comment above a field.
`ts{
// Using string path
removeComment(
// this will be removed
"foo": "bar"
},
"foo"
);
// ā '{ "foo": "bar" }'
// Using array path
removeComment(json, ["config", "enabled"]);
`
Get the comment associated with a field. First checks for a comment above, then falls back to a trailing comment.
`ts{
// Using string path
getComment(
// this is foo
"foo": "bar"
},
"foo"
);
// ā "this is foo"
// Using array path
getComment(
{
"foo": "bar" // trailing comment
},
["foo"]
);
// ā "trailing comment"
getComment('{ "foo": "bar" }', "foo");
// ā null (no comment)
`
Trailing comments are comments on the same line after a field value:
`jsonc`
{
"foo": "bar", // this is a trailing comment
"baz": 123 // another trailing comment
}
Get the trailing comment after a field (explicitly, ignoring comments above).
`ts{
// Using string path
getTrailingComment(
"foo": "bar", // trailing comment
"baz": 123
},
"foo"
);
// ā "trailing comment"
// Using array path
getTrailingComment(json, ["config", "database", "host"]);
`
Add or update a trailing comment after a field.
`ts{
// Using string path
setTrailingComment(
"foo": "bar",
"baz": 123
},
"foo",
"this is foo"
);
// ā '{ "foo": "bar" // this is foo, "baz": 123 }'
// Using array path
setTrailingComment(
{
"foo": "bar", // old comment
"baz": 123
},`
["foo"],
"new comment"
);
// ā replaces "old comment" with "new comment"
Remove the trailing comment after a field.
`ts{
// Using string path
removeTrailingComment(
"foo": "bar", // this will be removed
"baz": 123
},
"foo"
);
// ā '{ "foo": "bar", "baz": 123 }'
// Using array path
removeTrailingComment(json, ["config", "database", "host"]);
`
You can have both a comment above and a trailing comment:
`ts{
const json =
// comment above
"foo": "bar", // trailing comment
"baz": 123
};
getComment(json, "foo"); // ā "comment above" (prefers above)
getTrailingComment(json, "foo"); // ā "trailing comment"
// Set comment above (preserves trailing)
setComment(json, "foo", "new above");
// ā both comments preserved, above is updated
// Remove comment above (preserves trailing)
removeComment(json, "foo");
// ā trailing comment still there
`
When deleting fields, comments are deleted by default. Start a comment with ** to preserve it:
`ts{
remove(
// this comment will be deleted
"config": {}
},
"config"
);
// ā '{}' ā comment deleted with field
remove(
{
// ** this comment will be preserved
"config": {}
},`
"config"
);
// ā '{ // ** this comment will be preserved }' ā comment kept
Even though aywson works on strings, you can still do full object manipulation:
`ts
import { parse, set, remove, merge } from "aywson";
let json = {
// Database settings
"database": {
"host": "localhost",
"port": 5432
},
// Feature flags
"features": {
"darkMode": false,
"beta": true
}
};
// Parse to iterate/transform
const config = parse
// Example: Update all feature flags to false
for (const [key, value] of Object.entries(config.features as object)) {
if (typeof value === "boolean") {
json = set(json, features.${key}, false); // String path
// or: json = set(json, ["features", key], false); // Array path
}
}
// Example: Remove fields based on condition
for (const key of Object.keys(config)) {
if (key.startsWith("_")) {
json = remove(json, key); // String path
// or: json = remove(json, [key]); // Array path
}
}
// Example: Bulk update from transformed object
const updates = Object.fromEntries(
Object.entries(config.database as object).map(([k, v]) => [
k,
typeof v === "string" ? v.toUpperCase() : v
])
);
json = merge(json, { database: updates });
`
The key insight: use parse() to read and decide _what_ to change, then use set()/remove()/merge() to apply changes while preserving formatting and comments.
You can build a JSONC file from scratch using set() with comments:
`ts
import { set } from "aywson";
let json = "{}";
// Build up the structure with comments (using string paths)
json = set(json, "database", {}, "Database configuration");
json = set(json, "database.host", "localhost", "Primary database host");
json = set(json, "database.port", 5432);
json = set(json, "database.ssl", true, "Enable SSL in production");
json = set(json, "features", {}, "Feature flags");
json = set(json, "features.darkMode", false);
json = set(json, "features.beta", true, "Beta features - use with caution");
// Note: Array paths like ["database", "host"] are also supported
console.log(json);
`
Output:
`jsonc`
{
// Database configuration
"database": {
// Primary database host
"host": "localhost",
"port": 5432,
// Enable SSL in production
"ssl": true
},
// Feature flags
"features": {
"darkMode": false,
// Beta features - use with caution
"beta": true
}
}
For more complex construction, you can also use merge():
`ts
import { merge, setComment } from "aywson";
let json = "{}";
// Add multiple fields at once
json = merge(json, {
name: "my-app",
version: "1.0.0",
scripts: {
build: "tsc",
test: "vitest"
}
});
// Add comments where needed
json = setComment(json, "scripts", "Available npm scripts");
// Note: Array paths like ["scripts"] are also supported
`
`bashParse JSONC to JSON (strips comments, handles trailing commas)
aywson parse config.jsonc
Mutating commands always show a colored diff. Use
--dry-run (-n) to preview without writing.Path syntax: The CLI uses dot-notation:
config.database.host or bracket notation for indices: items[0].name. The API supports both string paths (same as CLI) and array paths: ["config", "database", "host"].$3
`bash
Path validation (prevents path traversal attacks)
aywson get config.json database.host # ā
Works
aywson get ../etc/passwd root # ā Blocked by default
aywson get --allow-path-traversal ../etc/passwd root # ā
Override (not recommended)File size limits (default: 50MB)
aywson parse large.json # ā
Works if < 50MB
aywson parse --max-file-size 100000000 large.json # ā
Custom limit (100MB)
aywson parse --no-file-size-limit huge.json # ā
Disable limit (not recommended)JSON parsing limits (via environment variables)
AYWSON_MAX_JSON_SIZE=20000000 aywson modify config.json '{"large": "data"}'
AYWSON_MAX_JSON_DEPTH=200 aywson merge config.json '{"deep": {"nested": {...}}}'
`Security
aywson includes several security features to protect against common attacks when processing untrusted input:
$3
By default, the CLI prevents path traversal attacks by validating that all file paths stay within the current working directory. This prevents access to files outside the intended directory (e.g.,
../etc/passwd).Override: Use the
--allow-path-traversal flag to bypass this protection (not recommended for untrusted input).`bash
Blocked by default
aywson get ../sensitive-file.json keyOverride (use with caution)
aywson get --allow-path-traversal ../sensitive-file.json key
`$3
To prevent memory exhaustion attacks, file size is limited by default to 50MB. Files larger than this limit will be rejected.
Override: Use
--max-file-size to set a custom limit, or --no-file-size-limit to disable the limit entirely.`bash
Default 50MB limit
aywson parse large.jsonCustom limit (100MB)
aywson parse --max-file-size 104857600 large.jsonNo limit (not recommended)
aywson parse --no-file-size-limit huge.json
`Note: Stdin (
-) is exempt from file size limits.$3
JSON input is validated for both size and nesting depth to prevent denial-of-service attacks:
- Default max size: 10MB
- Default max depth: 100 levels
Override: Set environment variables to customize these limits:
`bash
Increase JSON size limit to 20MB
AYWSON_MAX_JSON_SIZE=20971520 aywson modify config.json '{"large": "data"}'Increase depth limit to 200 levels
AYWSON_MAX_JSON_DEPTH=200 aywson merge config.json '{"deep": {...}}'Both limits
AYWSON_MAX_JSON_SIZE=20971520 AYWSON_MAX_JSON_DEPTH=200 aywson modify config.json '...'
`These limits apply to JSON arguments in
set, modify, and merge commands.$3
1. Don't disable security features unless you fully trust your input sources
2. Use appropriate limits for your use case rather than disabling them entirely
3. Validate input before passing it to aywson when processing untrusted data
4. Run with least privilege - don't run aywson as root or with elevated permissions
5. Keep dependencies updated - regularly update aywson and its dependencies for security patches
Comparison with
comment-jsoncomment-json is another popular package for working with JSON files that contain comments. Here's how the two packages differ:$3
| Aspect | aywson | comment-json |
| ------------------- | ------------------------------------- | -------------------------------------- |
| Approach | String-in, string-out | Parse to object, modify, stringify |
| Formatting | Preserves original formatting exactly | Re-stringifies (may change formatting) |
| Mutations | Immutable (returns new string) | Mutable (modifies object in place) |
| Comment storage | Stays in the string | Symbol properties on object |
$3
| Category | aywson | comment-json |
| --------------------- | ------------------------------------------------------------ | ------------------------------------ |
| Core |
parse() | parse(), stringify(), assign() |
| Path operations | get(), has(), set(), remove() | Object/array access |
| Bulk updates | merge(), modify() | assign() |
| Key manipulation | rename(), move(), sort() | ā |
| Comment API | getComment(), setComment(), getTrailingComment(), etc. | Symbol-based access |
| Comment positions | Above field and trailing (same line) | Many (before, after, inline, etc.) |
| Extras | CLI, ** prefix to preserve comments | CommentArray for array operations |$3
- You need exact formatting preservation (whitespace, indentation, trailing commas)
- You want surgical edits without re-serializing the entire file
- You prefer immutable operations that return new strings
- You need high-level operations like rename, move, or sort
- You want explicit comment manipulation with a simple API
$3
- You want to work with a JavaScript object and make many modifications before writing back
- You're comfortable with Symbol-based comment access
- Re-stringifying the entire file is acceptable for your use case
$3
comment-json:
`js
const { parse, stringify, assign } = require("comment-json");const obj = parse(jsonString);
obj.database.port = 5433;
assign(obj.database, { ssl: true });
const result = stringify(obj, null, 2);
`aywson:
`js
import { set, merge } from "aywson";let result = set(jsonString, "database.port", 5433);
result = merge(result, { database: { ssl: true } });
// Original formatting preserved exactly
``