A GitHub Action to modify values in JSON and JSONC files during workflows. Supports nested keys, typed values, deep merge, array indices, schema validation, and more.
Sometimes you need to update a .json file during a CI/CD workflow:
- Publish the same package to GitHub Packages (
@myorg/pkg) and npm (pkg) with different names - Bump a version number during a release
- Update a
tsconfig.jsoncompiler option before deployment - Set the
homepagefield for GitHub Pages
This action handles all of these by modifying your JSON file in-place, preserving formatting and comments.
- uses: maxgfr/github-change-json@main
with:
key: 'version'
value: '2.0.0'
path: package.jsonname: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set scoped name for GitHub Packages
uses: maxgfr/github-change-json@main
with:
key: 'name'
value: '@my-org/my-package'
path: package.json
- name: Bump version and add build metadata
uses: maxgfr/github-change-json@main
with:
path: package.json
changes: |
[
{"key": "version", "value": "2.0.0"},
{"key": "private", "value": "false", "type": "boolean"},
{"key": "scripts", "value": "{\"prepublish\": \"tsc\"}", "merge": true}
]
schema: schemas/package.schema.json
commit: true- uses: maxgfr/github-change-json@main
with:
key: 'compilerOptions.target'
value: 'ES2020'
path: tsconfig.jsonWorks with JSONC files (e.g. tsconfig.json with comments) -- comments are preserved.
By default values are strings. Use type for numbers, booleans, or JSON objects:
- uses: maxgfr/github-change-json@main
with:
key: 'port'
value: '3000'
type: 'number' # stored as 3000, not "3000"
path: config.json
- uses: maxgfr/github-change-json@main
with:
key: 'compilerOptions.strict'
value: 'true'
type: 'boolean' # stored as true, not "true"
path: tsconfig.json
- uses: maxgfr/github-change-json@main
with:
key: 'scripts'
value: '{"build": "tsc", "test": "jest"}'
type: 'json' # stored as an object, not a string
path: package.jsonNumeric path segments are treated as array indices:
- uses: maxgfr/github-change-json@main
with:
key: 'contributors.0.name' # first element of the array
value: 'Alicia'
path: package.jsonMerge new keys into an existing object without overwriting untouched keys:
- uses: maxgfr/github-change-json@main
with:
key: 'scripts'
value: '{"start": "node .", "deploy": "fly deploy"}'
merge: true
path: package.json
# {"build":"tsc","test":"jest"} + merge → {"build":"tsc","test":"jest","start":"node .","deploy":"fly deploy"}Nested objects are recursively merged; arrays and primitives are replaced.
- uses: maxgfr/github-change-json@main
with:
key: 'devDependencies'
path: package.json
delete: true- uses: maxgfr/github-change-json@main
with:
path: package.json
changes: |
[
{"key": "name", "value": "@my-org/my-package"},
{"key": "version", "value": "2.0.0"},
{"key": "private", "value": "true", "type": "boolean"},
{"key": "scripts", "value": "{\"deploy\": \"fly deploy\"}", "merge": true},
{"key": "devDependencies", "delete": true}
]Validate the result against a JSON Schema before writing. If validation fails, the file is not modified:
- uses: maxgfr/github-change-json@main
with:
key: 'version'
value: '2.0.0'
path: package.json
schema: schemas/package.schema.json # local file path
# or: schema: 'https://json.schemastore.org/package.json'Create the target file with {} if it doesn't exist yet (parent directories are created automatically):
- uses: maxgfr/github-change-json@main
with:
key: 'database.host'
value: 'localhost'
path: config/settings.json
create-if-missing: truePreview what would change without modifying the file:
- uses: maxgfr/github-change-json@main
with:
key: 'name'
value: '@my-org/my-package'
path: package.json
dry-run: true- uses: maxgfr/github-change-json@main
with:
key: 'name'
value: '@my-org/my-package'
path: package.json
commit: trueAdd a Signed-off-by trailer to the commit message for DCO compliance:
- uses: maxgfr/github-change-json@main
with:
key: 'version'
value: '2.0.0'
path: package.json
commit: true
signoff: true
# Commit message will include:
# Signed-off-by: <GITHUB_ACTOR> <<GITHUB_ACTOR>@users.noreply.github.com>- id: update
uses: maxgfr/github-change-json@main
with:
key: 'version'
value: '2.0.0'
path: package.json
- run: |
echo "Old: ${{ steps.update.outputs.old-value }}"
echo "New: ${{ steps.update.outputs.new-value }}"
- if: steps.update.outputs.modified == 'true'
run: echo "File changed, deploying..."| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | -- | Path to the JSON file (relative to repo root) |
key |
string | no* | -- | Key to modify. Supports dot notation for nesting (a.b.c) and array indices (items.0.name). Escape literal dots with \\ (my\\.key) |
value |
string | no* | -- | Value to set (always passed as a string, converted via type) |
type |
string | no | string |
Value type: string, number, boolean, or json |
commit |
boolean | no | false |
Commit and push changes |
signoff |
boolean | no | false |
Add Signed-off-by trailer to the commit message (DCO) |
delete |
boolean | no | false |
Delete the key instead of setting a value |
merge |
boolean | no | false |
Deep merge a JSON object into the existing value |
dry-run |
boolean | no | false |
Preview changes without writing to disk |
create-if-missing |
boolean | no | false |
Create the file with {} if it doesn't exist |
changes |
string | no | -- | JSON array of changes (overrides single-key inputs). Each item: {"key", "value", "type", "delete", "merge"} |
schema |
string | no | -- | Path or URL to a JSON Schema to validate the result against |
*Either key or changes is required. value is required unless delete: true.
| Name | Description |
|---|---|
old-value |
Previous value (string for single key, JSON object for multiple keys) |
new-value |
New value after modification (same format as old-value) |
modified |
'true' if the file content changed, 'false' otherwise |
Files with line comments (//), block comments (/* */), and trailing commas are fully supported. Comments are preserved when modifying values.
The action detects and preserves the original file's:
- Indentation (2 spaces, 4 spaces, tabs)
- Line endings (LF, CRLF)
- Trailing newline
- Runs before writing -- the file is never left in an invalid state
- Works in
dry-runmode too (validates the would-be result) - Supports local file paths and
http:///https://URLs (30s fetch timeout) - Uses JSON Schema draft-07 via Ajv
$refto external URLs within the schema is not supported
When commit: true:
- Git user name is set to
GITHUB_ACTOR(fallback:github-actions[bot]) - Git user email is set to
<GITHUB_ACTOR>@users.noreply.github.com(fallback:github-actions@users.noreply.github.com) - Commit message format:
- Single key:
chore: update <path> (set <key>=<value>)/(delete <key>)/(merge <key>=<value>) - Multiple changes:
chore: update <path> with N changes - Long values are truncated to 50 characters in the commit message
- Single key:
- Pushed to
GITHUB_HEAD_REF(PR source branch) orGITHUB_REF(current ref) as fallback - Pre-commit hooks are bypassed (
--no-verify) - When
signoff: true, addsSigned-off-by: Name <email>trailer via--signoff - Skipped in
dry-runmode
The action fails with a clear message when:
- File not found (and
create-if-missingisfalse) - Invalid JSON/JSONC syntax in the target file
- Invalid type conversion (
type: numberwithvalue: abc,NaN,Infinity) - Invalid
typevalue (anything other thanstring,number,boolean,json) - Conflicting flags (
delete+mergeboth true) - Non-string
valueinchangesarray (e.g.{"value": 42}instead of{"value": "42"}) - Missing required fields (
keyorvaluewhen needed) - Invalid
changesinput (not valid JSON, not an array, missingkey) - Merge with non-JSON or non-object value
- Schema validation failure (with detailed per-field error messages)
- Schema file not found, invalid JSON, or invalid schema structure
- Schema URL fetch failure or timeout (30s)
- Setting a nested path through a primitive (
name.subwhennameis a string)
- String key modifications require an object root (
{}), not an array root ([]) - Purely numeric path segments are always array indices -- string keys like
"0"are not supported - Merge requires a JSON object value (not arrays or primitives)
- Schema
$refto external URLs is not resolved
pnpm install # install dependencies
pnpm run build # compile TypeScript
pnpm run package # bundle with ncc
pnpm run lint # run ESLint
pnpm run format # format with Prettier
pnpm test # run 165 tests
pnpm run all # build + package + lint + testMIT
Contributions are welcome! Please feel free to submit a Pull Request.