Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool
Creating Custom JSON Formatter Rules in ESLint
Maintaining consistent formatting and structure across all files in a project is crucial for readability and collaboration. While ESLint is primarily known for linting JavaScript and TypeScript, it can also be extended to enforce rules on other file types, including JSON.
Standard JSON formatters exist, but sometimes you need to enforce specific rules that go beyond simple indentation or spacing – rules about the presence of certain keys, the order of properties, or the format of values. This is where custom ESLint rules become invaluable.
Why Custom JSON Rules?
You might need custom JSON rules for various reasons:
- Configuration Consistency: Enforce specific structures or required fields in configuration files (e.g.,
package.json
,tsconfig.json
, custom config files). - API Contract Validation: If you define API payloads or data structures in JSON, linting can help ensure they conform to expected formats.
- Localization Files: Ensure translation files have all necessary keys and consistent structures.
- Code Generation Input: Validate JSON files used as input for code generation processes.
ESLint Basics & JSON
ESLint works by parsing code into an Abstract Syntax Tree (AST) and then traversing this tree, allowing rules to visit specific node types (like function calls, variable declarations, object properties). For JavaScript, ESLint uses parsers like Esprima, Acorn, or@typescript-eslint/parser.
To lint file types other than standard JavaScript/TypeScript, ESLint relies on processors. A processor extracts code from a file (or transforms it) so ESLint can lint it. For JSON files, the most common approach is to use a plugin that includes a processor and defines rules specifically for JSON AST. The de facto standard for this is eslint-plugin-jsonc(or the older, less maintained eslint-plugin-json). We will focus on the concepts applicable when using such a plugin, specifically how to write a rule that understands a JSON AST.
eslint-plugin-jsonc
provides a parser and processor that turns JSON text into an AST similar to ESTree (the standard AST for JS), but with node types relevant to JSON (e.g., JSONObjectExpression
, JSONArrayExpression
, JSONProperty
, JSONIdentifier
, JSONLiteral
).
Structure of a Custom Rule
An ESLint rule is an object with two main parts:
meta
: Provides metadata about the rule (type, description, fixability, schema for options).create
: A function that returns an object where keys are AST node types (or selectors) and values are functions that ESLint calls when traversing the AST and encountering a matching node.
For JSON rules using eslint-plugin-jsonc
, you'll subscribe to node types like JSONObjectExpression
, JSONProperty
, JSONLiteral
, etc.
Creating a Custom Rule for JSON
Let's outline the steps and provide examples.
1. Setup: Project Structure & Plugin
Custom rules are typically bundled in an ESLint plugin. For project-specific rules, you can create a local plugin directory.
Example Local Plugin Structure:
my-eslint-plugin/ ├── index.js <-- Defines the plugin └── rules/ └── my-json-rule.js <-- Your custom rule file
my-eslint-plugin/index.js
:
module.exports = { rules: { "my-json-rule": require("./rules/my-json-rule"), // Add other rules here }, // Processors can also be defined here if needed, // but for JSON, we rely on eslint-plugin-jsonc's processor. };
2. Define the Rule File (`my-json-rule.js`)
This file will contain the rule definition object.
// my-eslint-plugin/rules/my-json-rule.js module.exports = { meta: { type: "problem", // or "suggestion", "layout" docs: { description: "Enforce a custom rule for JSON objects.", category: "Possible Problems", // Or "Suggestions", "Layout & Formatting" recommended: false, url: "https://example.com/docs/my-json-rule", // Optional documentation URL }, fixable: "code", // or "whitespace" or null schema: [], // Define options schema here }, create: function (context) { // Rule logic goes here, returning visitor functions return { // Visitor function for a specific JSON AST node type // Example: checking the root object JSONObjectExpression(node) { // 'node' is the AST node for the top-level JSON object // 'context' provides methods like report() // console.log('Visited a JSON Object:', node); // Example check: Ensure the root object has a 'version' key const versionProperty = node.properties.find(prop => prop.key.type === 'JSONIdentifier' && prop.key.name === 'version' ); if (!versionProperty) { context.report({ node: node, // Report on the object node message: "JSON object must contain a 'version' key.", }); } // Example check: Ensure 'version' value is a string if (versionProperty && versionProperty.value.type !== 'JSONLiteral' && typeof versionProperty.value.value !== 'string') { context.report({ node: versionProperty.value, // Report on the value node message: "'version' key must have a string value.", }); } }, // You can define more visitor functions for other node types // JSONProperty(node) { // // 'node' is an AST node for a key-value pair within an object // // console.log('Visited a JSON Property:', node.key.name); // }, // JSONArrayExpression(node) { // // 'node' is an AST node for an array // } }; }, };
Key parts:
meta.type
: How ESLint should treat issues from this rule.meta.docs
: Description and category.meta.fixable
: Indicates if ESLint can automatically fix the issue.meta.schema
: Defines options the rule accepts.create(context)
: The factory function.context
provides methods likecontext.report()
to flag issues.- Visitor functions (e.g.,
JSONObjectExpression(node)
): These are called during the AST traversal. Thenode
argument is the AST node being visited.
3. Example: Enforcing Key Order
Let's create a rule that checks if properties in a JSON object are sorted alphabetically.
my-eslint-plugin/rules/sort-json-keys.js
:
// my-eslint-plugin/rules/sort-json-keys.js module.exports = { meta: { type: "layout", docs: { description: "Enforce alphabetical sorting of keys in JSON objects.", category: "Layout & Formatting", recommended: false, }, fixable: "code", // This rule can potentially auto-fix schema: [], // Simple rule, no options needed }, create: function (context) { return { // Visitor for JSON object nodes JSONObjectExpression(node) { const properties = node.properties; if (properties.length <= 1) { // No need to sort if 0 or 1 property return; } // Get the names of the keys as strings const keyNames = properties.map(prop => { // Handle different key types (Identifier or Literal) if (prop.key.type === 'JSONIdentifier' || (prop.key.type === 'JSONLiteral' && typeof prop.key.value === 'string')) { return prop.key.name || prop.key.value; // Use name for Identifier, value for string Literal } // For other literal types (numbers, booleans), treat as string if (prop.key.type === 'JSONLiteral') { return String(prop.key.value); } // If key is not a simple type we can compare (e.g., object key), ignore return null; }).filter(name => name !== null); // Filter out non-comparable keys // Create a sorted version of the key names const sortedKeyNames = [...keyNames].sort((a, b) => a.localeCompare(b)); // Compare the original order with the sorted order for (let i = 0; i < keyNames.length; i++) { if (keyNames[i] !== sortedKeyNames[i]) { // Found a key that is out of order const currentKeyNode = properties[i].key; const expectedKeyName = sortedKeyNames[i]; context.report({ node: currentKeyNode, // Report on the key that is out of order message: "Keys must be sorted alphabetically. Expected '{{expected}}' before '{{found}}'.", data: { expected: expectedKeyName, found: keyNames[i], }, // Optional: Add a fixer function for auto-fixing // This is more complex as it involves rearranging nodes. // A simple example fixer for a property: // fixer: function(fixer) { // // This would require more sophisticated logic // // to rearrange properties in the parent object. // // Usually, for sorting, it's easier to report and let a formatter handle it // // or implement a more complex multi-node fixer. // return null; // No fix provided for this simple example // } }); // Report only the first out-of-order key in this object break; } } }, }; }, };
This rule iterates through the properties of each object it encounters. It extracts the key names, sorts them, and then compares the original order to the sorted order. If a key is found out of place, it reports an error. Implementing the fixer
function for sorting is more complex as it needs to know the source code range of multiple properties to rearrange them.
4. Example: Validating Specific Key Values
Let's create a rule that checks if the "version"
key in package.json
matches a specific pattern (e.g., semver).
my-eslint-plugin/rules/valid-package-version.js
:
// my-eslint-plugin/rules/valid-package-version.js // Requires eslint-plugin-jsonc to provide the JSON AST node types // Simple regex for basic semver (major.minor.patch) // Does NOT cover pre-release or build metadata const SEMVER_REGEX = /^\d+\.\d+\.\d+$/; module.exports = { meta: { type: "problem", docs: { description: "Enforce semver format for package.json version.", category: "Possible Problems", recommended: false, }, fixable: null, // Cannot auto-fix version format schema: [], }, create: function (context) { return { // Selects a JSONProperty node with a key named "version" // This is a more specific selector than just JSONProperty 'JSONProperty[key.name="version"]'(node) { // 'node' is the AST node for the "version": "..." property const valueNode = node.value; // Check if the value is a string literal if (valueNode.type === 'JSONLiteral' && typeof valueNode.value === 'string') { const version = valueNode.value; // Check if the string matches the semver regex if (!SEMVER_REGEX.test(version)) { context.report({ node: valueNode, // Report on the value node message: "Package version '{{version}}' must follow semver format (major.minor.patch).", data: { version: version, }, }); } } else { // Report if the value is not a string literal context.report({ node: valueNode, // Report on the value node message: "Package version must be a string literal.", }); } }, }; }, };
This rule uses an AST selector string 'JSONProperty[key.name="version"]'
to target only property nodes whose key is named "version". Inside the visitor function, it accesses the value
node of the property and checks if it's a string literal matching a basic semver pattern.
5. Integrate into ESLint Configuration
Once you have your local plugin, you need to tell ESLint about it and configure the rules.
Example .eslintrc.js
or .eslintrc.json
:
// .eslintrc.js const path = require('path'); module.exports = { // ... other ESLint configurations // Tell ESLint where to find your local plugin plugins: [ "jsonc", // Add the jsonc plugin first require(path.resolve(__dirname, 'my-eslint-plugin')), // Path to your local plugin ], // Define overrides to apply rules specifically to JSON files overrides: [ { files: ["*.json", "*.jsonc"], // Target JSON and JSONC files parser: "jsonc-eslint-parser", // Use the jsonc parser rules: { // Configure jsonc plugin's recommended rules (optional) // "jsonc/recommended-with-json": "warn", // "jsonc/recommended-with-jsonc": "warn", // "jsonc/auto": "warn", // Or "jsonc/auto" for automatically picking settings // Configure your custom rules "my-eslint-plugin/my-json-rule": "error", // Use the rule name from index.js "my-eslint-plugin/sort-json-keys": "warn", "my-eslint-plugin/valid-package-version": [ "error", // Enable rule with severity "error" // You could pass options here if your rule schema defined them // { "pattern": "^v\d+\.\d+\.\d+$" } ], // You can also use other jsonc rules "jsonc/indent": ["error", 2], // Enforce 2-space indentation "jsonc/no-bigint-literals": "error", // Disallow BigInt (if needed) // etc. }, }, // Add other overrides if you have different rules for different JSON files // { // files: ["package.json"], // rules: { // "my-eslint-plugin/valid-package-version": "error", // Apply specifically to package.json // // Other package.json specific rules // } // } ], // ... rest of your config };
Important points for JSON linting:
- Include
"jsonc"
in theplugins
array. - Include your local plugin path in the
plugins
array. - Use the
overrides
section to specify rules that only apply to JSON files. - Inside the JSON override, set
parser: "jsonc-eslint-parser"
. - Configure your custom rules using the format
"plugin-name/rule-name"
.
6. Running ESLint
With the configuration set up, you can run ESLint from your terminal:
# Lint specific JSON files npx eslint path/to/your/file.json # Lint all JSON files matching a pattern npx eslint "**/*.json" # Lint all files in the project npx eslint .
ESLint will use the overrides to apply the JSON parser and your custom rules to the specified files.
Advanced Considerations
- Fixers: Implementing auto-fixing (`meta.fixable`) requires careful use of the
fixer
object provided in thecontext.report
function. For simple changes (like adding a missing key), it's straightforward. For complex changes (like sorting), it's much harder and often relies on replacing larger sections of code or even the entire file content based on the desired structure. - Options: Use
meta.schema
to define options for your rule, allowing users to customize its behavior (e.g., specifying the required keys or the sorting order). Access options viacontext.options
. - Testing: Thoroughly test your rules using ESLint's
RuleTester
. This involves providing valid and invalid code examples and asserting that the rule reports errors correctly (and fixes them if applicable). - JSON with Comments/Trailing Commas (JSONC):
eslint-plugin-jsonc
also handles JSONC format. If your files allow comments, make sure your rules are robust enough to handle them or only apply the rules to standard JSON files.
Conclusion
While the standard JSON specification is quite simple, the JSON files used in real-world projects often have specific structural or content requirements. By leveraging ESLint's plugin architecture and a processor like eslint-plugin-jsonc
, you can create powerful custom rules to enforce these project-specific conventions. This ensures greater consistency, reduces errors, and improves the maintainability of your configuration and data files, just like ESLint does for your code.
Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool