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

If you landed here looking for a current guide, the big change is that modern ESLint setups use eslint.config.js and now have an official JSON language plugin. That means many older tutorials built around .eslintrc, processors, or the legacy eslint-plugin-json package are no longer the best starting point.

For most teams, the practical workflow is simple: use a built-in JSON sorting rule if it already matches your formatting convention, and only write a custom rule when you need project-specific behavior such as required top-level keys, file-specific key order, or schema-like checks for configuration files.

What Changed in Current ESLint Setups

  • Flat config is the default: New ESLint guidance centers on eslint.config.js, not .eslintrc.*.
  • ESLint has an official JSON plugin: @eslint/json supports JSON, JSONC, and JSON5 through ESLint's language-plugin system.
  • eslint-plugin-jsonc is still very useful: It remains the easiest path when you want a JSON AST that feels similar to JavaScript rule authoring and when you want mature JSON formatting rules such as jsonc/sort-keys.

Pick the Right JSON Stack

There are now two realistic choices for custom JSON linting in ESLint:

  • Use @eslint/json if you want the official ESLint path, are already on flat config, and are comfortable working with the Momoa JSON AST. This path requires modern ESLint and does not use the older parser-plus-processor pattern.
  • Use eslint-plugin-jsonc if you want JSON node names and selectors that feel closer to typical ESLint rule examples, such as JSONObjectExpression and JSONProperty.
  • Avoid eslint-plugin-json for new custom-rule workbecause it is processor-based and does not expose an AST you can target with normal ESLint visitors.

For the rest of this guide, I'll use eslint-plugin-jsonc because it is the most approachable way to create custom JSON formatter rules today. If you later move to @eslint/json, the same rule ideas still apply, but your AST node types and test setup will differ.

Try Built-In Rules Before Writing Code

Many "custom formatter rule" requests are already covered by existing rules. That matters because a built-in rule is cheaper to maintain than your own plugin code.

Example: sort top-level keys in package.json

// eslint.config.js
import jsonc from "eslint-plugin-jsonc";

export default [
  ...jsonc.configs["recommended-with-jsonc"],
  {
    files: ["package.json"],
    plugins: { jsonc },
    rules: {
      "jsonc/sort-keys": [
        "error",
        {
          pathPattern: "^$",
          order: [
            "name",
            "version",
            "private",
            "type",
            "scripts",
            "dependencies",
            "devDependencies"
          ]
        }
      ]
    }
  }
];

The jsonc/sort-keys rule is often enough for formatter-style requirements. It can target specific object paths, and its fixer may need more than one eslint --fix pass for complex reordering. Only move on to a custom rule when your convention goes beyond what built-ins can express.

Current Setup for a Custom JSON Rule

In a flat-config project, the simplest pattern is to define a small local plugin object and register your custom rules under a namespace such as local.

eslint.config.js

import jsonc from "eslint-plugin-jsonc";
import requireTopLevelKeys from "./eslint/rules/require-top-level-keys.js";

const localJsonRules = {
  meta: { name: "local-json-rules" },
  rules: {
    "require-top-level-keys": requireTopLevelKeys
  }
};

export default [
  ...jsonc.configs["recommended-with-jsonc"],
  {
    files: ["package.json", "tsconfig*.json", ".vscode/*.json"],
    plugins: {
      jsonc,
      local: localJsonRules
    },
    rules: {
      "jsonc/sort-keys": [
        "error",
        {
          pathPattern: "^$",
          order: ["name", "version", "private"]
        }
      ],
      "local/require-top-level-keys": [
        "error",
        {
          keys: ["name", "version"]
        }
      ]
    }
  }
];

This setup does three useful things at once: it parses JSON files correctly, keeps ordinary formatting rules in configuration instead of code, and gives you a place to add genuinely project-specific logic.

Example Custom Rule: Require Top-Level Keys

A good first custom rule is one that checks structure rather than whitespace. The example below enforces that root-level config files contain required keys like name and version.

eslint/rules/require-top-level-keys.js

function getKeyName(keyNode) {
  if (keyNode.type === "JSONLiteral") {
    return String(keyNode.value);
  }

  if ("name" in keyNode && typeof keyNode.name === "string") {
    return keyNode.name;
  }

  return null;
}

export default {
  meta: {
    type: "problem",
    docs: {
      description: "Require specific keys in the root JSON object."
    },
    schema: [
      {
        type: "object",
        properties: {
          keys: {
            type: "array",
            items: { type: "string" },
            minItems: 1,
            uniqueItems: true
          }
        },
        required: ["keys"],
        additionalProperties: false
      }
    ],
    messages: {
      missingKey: "Missing required top-level key '{{key}}'."
    }
  },

  create(context) {
    const [{ keys = [] } = {}] = context.options;
    let checkedRootObject = false;

    return {
      JSONObjectExpression(node) {
        if (checkedRootObject) {
          return;
        }

        checkedRootObject = true;

        const existingKeys = new Set(
          node.properties
            .map((property) => getKeyName(property.key))
            .filter(Boolean)
        );

        for (const key of keys) {
          if (!existingKeys.has(key)) {
            context.report({
              node,
              messageId: "missingKey",
              data: { key }
            });
          }
        }
      }
    };
  }
};

This example assumes the file's top-level JSON value is an object, which is exactly what you have in package.json, tsconfig.json, and editor settings files. That makes it a strong pattern for real configuration linting even though it stays small enough to understand quickly.

Test the Rule Before You Trust It

Custom rules are easy to break when you later add options or fixers. A short RuleTester suite is usually enough to lock down the behavior.

import { RuleTester } from "eslint";
import jsoncParser from "jsonc-eslint-parser";
import rule from "./require-top-level-keys.js";

const tester = new RuleTester({
  languageOptions: {
    parser: jsoncParser
  }
});

tester.run("require-top-level-keys", rule, {
  valid: [
    {
      code: '{ "name": "demo", "version": "1.0.0" }',
      options: [{ keys: ["name", "version"] }]
    }
  ],
  invalid: [
    {
      code: '{ "name": "demo" }',
      options: [{ keys: ["name", "version"] }],
      errors: [
        {
          messageId: "missingKey",
          data: { key: "version" }
        }
      ]
    }
  ]
});

If you decide to use @eslint/json instead, keep the same habit of testing but expect different AST shapes because the official plugin is built on Momoa rather than the JSONC parser's ESTree-like node model.

When the Official @eslint/json Path Makes More Sense

If your team prefers to stay close to ESLint's official direction, @eslint/json is a solid choice. It supports JSON, JSONC, and JSON5 in flat config and includes a built-in sort-keys rule. The tradeoff is that custom-rule examples written for eslint-plugin-jsonc will not map one-to-one because the AST is different.

Minimal official-plugin example

import { defineConfig } from "eslint/config";
import json from "@eslint/json";

export default defineConfig([
  {
    files: ["**/*.json"],
    plugins: { json },
    language: "json/json",
    extends: ["json/recommended"],
    rules: {
      "json/sort-keys": "error"
    }
  }
]);

Troubleshooting and Practical Notes

  • If your JSON rule never runs, check the basics first: matching files globs, the correct JSON parser or language, and flat config instead of old .eslintrc examples copied from blog posts.
  • If you only need key ordering, indentation, trailing-comma rules, or quote enforcement, prefer existing JSON lint rules over custom code.
  • If you lint JSON in VS Code, the ESLint extension may need json, jsonc, and json5 added to eslint.validate before editor feedback appears.
  • If you maintain a custom fixer that rewrites object order, test it on comments and trailing commas explicitly. Those are the edge cases most likely to corrupt JSONC files.

Bottom Line

The current way to create custom JSON formatter rules in ESLint is to start with flat config, use built-in sorting rules wherever possible, and add a small local plugin only for conventions that are genuinely unique to your project. For most rule authors, eslint-plugin-jsonc is still the fastest way to get there, while @eslint/json is the official option when you want to align with ESLint's newer language-plugin model.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool