Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(linting): introduce jsonschema cli for linting and autofixing li…
…nting issues in schemas

Signed-off-by: karan-palan <[email protected]>
  • Loading branch information
Karan-Palan committed Sep 22, 2025
commit b0bdc353aca1cf00c0e8c59ca59ab2af492418b4
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,30 @@ Options:
--defaultNumberType Default number type. [choices: "number", "integer"] [default: "number"]
--tsNodeRegister Use ts-node/register (needed for require typescript files). [boolean] [default: false]
--constAsEnum Use enums with a single value when declaring constants. [boolean] [default: false]
--lint Lint generated schemas for JSON Schema best practices. [boolean] [default: false]
--fix Automatically fix linting issues in generated schemas. [boolean] [default: false]
--lintStrict Enable strict linting rules for generated schemas. [boolean] [default: false]
--experimentalDecorators Use experimentalDecorators when loading typescript modules.
[boolean] [default: true]
```

#### JSON Schema Linting

This tool integrates with [@sourcemeta/jsonschema](https://github.com/sourcemeta/jsonschema) to provide JSON Schema linting capabilities. You can automatically validate and fix your generated schemas to follow JSON Schema best practices.

**Example usage:**

```bash
# Generate schema with linting enabled
typescript-json-schema types.ts MyType --lint --out schema.json

# Generate schema with automatic fixes applied
typescript-json-schema types.ts MyType --fix --out schema.json

# Generate schema with strict linting rules
typescript-json-schema types.ts MyType --lint --lintStrict --out schema.json
```

### Programmatic use

```ts
Expand All @@ -66,6 +86,8 @@ import * as TJS from "typescript-json-schema";
// optionally pass argument to schema generator
const settings: TJS.PartialArgs = {
required: true,
lint: true, // Enable linting
fix: true, // Automatically fix linting issues
};

// optionally pass ts compiler options
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
"description": "typescript-json-schema generates JSON Schema files from your Typescript sources",
"main": "dist/typescript-json-schema.js",
"typings": "dist/typescript-json-schema.d.ts",
"bin": {
"typescript-json-schema": "./bin/typescript-json-schema"
},
"bin": "./bin/typescript-json-schema",
"author": "Yousef El-Dardiry and Dominik Moritz",
"contributors": [
{
Expand Down Expand Up @@ -51,6 +49,7 @@
"yargs": "^17.1.1"
},
"devDependencies": {
"@sourcemeta/jsonschema": "^11.8.2",
"@types/chai": "^4.2.21",
"@types/glob": "^7.1.4",
"@types/mocha": "^9.0.0",
Expand All @@ -70,8 +69,11 @@
"run": "ts-node typescript-json-schema-cli.ts",
"build": "tsc",
"lint": "tslint --project tsconfig.json -c tslint.json --exclude '**/*.d.ts'",
"lint:schemas": "find test/programs -name '*.json' -exec npx @sourcemeta/jsonschema lint {} \\;",
"fix:schemas": "find test/programs -name '*.json' -exec npx @sourcemeta/jsonschema lint --fix {} \\;",
"style": "prettier --write *.js *.ts test/*.ts",
"dev": "tsc -w",
"test:dev": "mocha -t 5000 --watch --require source-map-support/register dist/test"
}
},
"packageManager": "[email protected]"
}
37 changes: 37 additions & 0 deletions test/lint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { assert } from "chai";
import { exec, getDefaultArgs } from "../typescript-json-schema";
import * as fs from "fs";

describe("schema linting", () => {
const testSchemaPath = "./test-lint-output.json";

afterEach(() => {
try {
if (fs.existsSync(testSchemaPath)) {
fs.unlinkSync(testSchemaPath);
}
} catch (error) {
}
});

it("should generate schema with linting", async () => {
const args = {
...getDefaultArgs(),
out: testSchemaPath,
lint: true,
fix: false
};

try {
await exec("test/programs/interface-single/main.ts", "MyObject", args);
assert.isTrue(fs.existsSync(testSchemaPath), "Schema file should be generated");
} catch (error: any) {
if (error.message.includes("jsonschema")) {
console.warn("Skipping linting test: CLI not available");
assert.isTrue(true, "Test skipped");
} else {
throw error;
}
}
});
});
9 changes: 9 additions & 0 deletions typescript-json-schema-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export function run() {
.describe("tsNodeRegister", "Use ts-node/register (needed for requiring typescript files).")
.boolean("constAsEnum").default("constAsEnum", defaultArgs.constAsEnum)
.describe("constAsEnum", "Use enums with a single value when declaring constants. Needed for OpenAPI compatibility")
.boolean("lint").default("lint", defaultArgs.lint)
.describe("lint", "Lint generated schemas for JSON Schema best practices and report issues")
.boolean("fix").default("fix", defaultArgs.fix)
.describe("fix", "Automatically fix linting issues in generated schemas")
.boolean("lintStrict").default("lintStrict", defaultArgs.lintStrict)
.describe("lintStrict", "Enable strict linting rules (use with --fix to apply strict fixes)")
.argv;

exec(args._[0], args._[1], {
Expand Down Expand Up @@ -84,6 +90,9 @@ export function run() {
defaultNumberType: args.defaultNumberType,
tsNodeRegister: args.tsNodeRegister,
constAsEnum: args.constAsEnum,
lint: args.lint,
fix: args.fix,
lintStrict: args.lintStrict,
});
}

Expand Down
101 changes: 97 additions & 4 deletions typescript-json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { createHash } from "crypto";
import * as ts from "typescript";
import { JSONSchema7, JSONSchema7TypeName } from "json-schema";
import { pathEqual } from "path-equal";
import { execSync } from "child_process";
import * as fs from "fs";
import * as os from "os";
export { Program, CompilerOptions, Symbol } from "typescript";

const vm = require("vm");
Expand Down Expand Up @@ -61,6 +64,9 @@ export function getDefaultArgs(): Args {
defaultNumberType: "number",
tsNodeRegister: false,
constAsEnum: false,
lint: false,
fix: false,
lintStrict: false,
};
}

Expand Down Expand Up @@ -93,6 +99,9 @@ export type Args = {
defaultNumberType: "number" | "integer";
tsNodeRegister: boolean;
constAsEnum: boolean;
lint: boolean;
fix: boolean;
lintStrict: boolean;
};

export type PartialArgs = Partial<Args>;
Expand Down Expand Up @@ -1828,6 +1837,73 @@ function normalizeFileName(fn: string): string {
return fn;
}

async function lintSchema(schemaJson: string, options: { fix?: boolean; strict?: boolean } = {}): Promise<string> {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsonschema-lint-'));
const tempFile = path.join(tempDir, 'schema.json');

try {
fs.writeFileSync(tempFile, schemaJson);

const jsonschemaCmd = path.join(process.cwd(), 'node_modules/.bin/jsonschema');
let cmd = `"${jsonschemaCmd}" lint "${tempFile}"`;

if (options.fix) {
cmd += ' --fix';
}

if (options.strict) {
cmd += ' --strict';
}

try {
const result = execSync(cmd, {
stdio: 'pipe',
encoding: 'utf8'
});

if (!options.fix && result) {
console.log('JSON Schema lint results:');
console.log(result);
} else if (options.fix) {
console.log('JSON Schema auto-fix completed successfully.');
if (result) {
console.log(result);
}
}

} catch (error: any) {
if (error.status === 1) {
const output = error.stdout || error.stderr || '';

if (!options.fix) {
console.error('JSON Schema linting issues found:');
console.error(output);
console.log('Run with --fix to automatically fix these issues.');
} else {
console.log('JSON Schema linting issues were found and fixed:');
if (output) {
console.log(output);
}
}
} else {
throw new Error(`JSON Schema linting failed: ${error.message}`);
}
}

const lintedSchema = fs.readFileSync(tempFile, 'utf8');
return lintedSchema;

} catch (error: any) {
throw new Error(`Failed to lint schema: ${error.message}`);
} finally {
try {
fs.unlinkSync(tempFile);
fs.rmdirSync(tempDir);
} catch (error) {
}
}
}

export async function exec(filePattern: string, fullTypeName: string, args = getDefaultArgs()): Promise<void> {
let program: ts.Program;
let onlyIncludeFiles: string[] | undefined = undefined;
Expand All @@ -1854,15 +1930,32 @@ export async function exec(filePattern: string, fullTypeName: string, args = get
throw new Error("No output definition. Probably caused by errors prior to this?");
}

const json = stringify(definition, null, 4) + "\n\n";
let json = stringify(definition, null, 4) + "\n\n";

if (args.lint || args.fix) {
try {
json = await lintSchema(json, {
fix: args.fix,
strict: args.lintStrict
});
} catch (error: any) {
if (args.ignoreErrors) {
console.warn('Schema linting failed:', error.message);
console.warn('Proceeding with original schema due to ignoreErrors flag...');
} else {
throw error;
}
}
}

if (args.out) {
return new Promise((resolve, reject) => {
const fs = require("fs");
fs.mkdir(path.dirname(args.out), { recursive: true }, function (mkErr: Error) {
const fsModule = require("fs");
fsModule.mkdir(path.dirname(args.out), { recursive: true }, function (mkErr: Error) {
if (mkErr) {
return reject(new Error("Unable to create parent directory for output file: " + mkErr.message));
}
fs.writeFile(args.out, json, function (wrErr: Error) {
fsModule.writeFile(args.out, json, function (wrErr: Error) {
if (wrErr) {
return reject(new Error("Unable to write output file: " + wrErr.message));
}
Expand Down
Loading