Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
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": "yarn@4.10.2"
}
38 changes: 38 additions & 0 deletions test/lint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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) {
console.warn(`Warning: Failed to clean up test file: ${error instanceof Error ? error.message : 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;
}
}
});
});
42 changes: 28 additions & 14 deletions test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function assertRejection(
type: string,
settings: TJS.PartialArgs = {},
compilerOptions?: TJS.CompilerOptions,
errType?: RegExp | ErrorConstructor,
errType?: RegExp | ErrorConstructor
) {
it(group + " should reject input", () => {
let schema = null;
Expand Down Expand Up @@ -181,8 +181,8 @@ describe("interfaces", () => {
const schema = generator?.getSchemaForSymbol("MySubObject");

// Should not change original schema object.
assert.deepEqual(schemaOverride1, { type: "string" });
assert.deepEqual(schemaOverride2, { type: "integer" });
assert.deepEqual(schemaOverride1, { type: "string" });
assert.deepEqual(schemaOverride2, { type: "integer" });

assert.deepEqual(schema, { ...schemaOverride1, $schema: "http://json-schema.org/draft-07/schema#" });
});
Expand All @@ -198,20 +198,20 @@ describe("interfaces", () => {
const schema = generator?.getSchemaForSymbol("MySubObject1", true, true);

// Should not change original schema object.
assert.deepEqual(schemaOverride1, { type: "string" });
assert.deepEqual(schemaOverride2, { type: "integer" });
assert.deepEqual(schemaOverride1, { type: "string" });
assert.deepEqual(schemaOverride2, { type: "integer" });

assert.deepEqual(schema, {
...schemaOverride1,
$schema: "http://json-schema.org/draft-07/schema#",
definitions: {
MySubObject1: {
type: "string"
type: "string",
},
MySubObject2: {
type: "integer"
}
}
type: "integer",
},
},
});
});
it("should ignore type aliases that have schema overrides", () => {
Expand Down Expand Up @@ -475,7 +475,13 @@ describe("schema", () => {
assertSchema("undefined-property", "MyObject");

// Creating a schema for main type = undefined should fail
assertRejection("type-alias-undefined", "MyUndefined", undefined, undefined, /Not supported: root type undefined/);
assertRejection(
"type-alias-undefined",
"MyUndefined",
undefined,
undefined,
/Not supported: root type undefined/
);
});

describe("other", () => {
Expand Down Expand Up @@ -573,13 +579,17 @@ describe("when reusing a generator", () => {
const program = TJS.programFromConfig(resolve(testProgramPath + "tsconfig.json"));
const generator = TJS.buildGenerator(program);

["MyObject", "MyOtherObject"].forEach(symbolName => {
["MyObject", "MyOtherObject"].forEach((symbolName) => {
const expectedSchemaString = readFileSync(testProgramPath + `schema.${symbolName}.json`, "utf8");
const expectedSchemaObject = JSON.parse(expectedSchemaString);

const actualSchemaObject = generator?.getSchemaForSymbol(symbolName);

assert.deepEqual(actualSchemaObject, expectedSchemaObject, `The schema for ${symbolName} is not as expected`);
assert.deepEqual(
actualSchemaObject,
expectedSchemaObject,
`The schema for ${symbolName} is not as expected`
);
});
});

Expand All @@ -600,7 +610,11 @@ describe("when reusing a generator", () => {

const actualSchemaObject = generator?.getSchemaForSymbol(symbolName);

assert.deepEqual(actualSchemaObject, expectedSchemaObject, `The schema for ${symbolName} is not as expected`);
assert.deepEqual(
actualSchemaObject,
expectedSchemaObject,
`The schema for ${symbolName} is not as expected`
);
});
});

Expand All @@ -621,7 +635,7 @@ describe("when reusing a generator", () => {
});
});

describe("satisfies keyword - ignore from a \"satisfies\" and build by rally type", () => {
describe('satisfies keyword - ignore from a "satisfies" and build by rally type', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use consistent quotes

assertSchema("satisfies-keyword", "Specific");
});

Expand Down
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
Loading