Skip to content
6 changes: 5 additions & 1 deletion factory/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { SatisfiesNodeParser } from "../src/NodeParser/SatisfiesNodeParser.js";
import { PromiseNodeParser } from "../src/NodeParser/PromiseNodeParser.js";
import { SpreadElementNodeParser } from "../src/NodeParser/SpreadElementNodeParser.js";
import { IdentifierNodeParser } from "../src/NodeParser/IdentifierNodeParser.js";
import { castArray } from "../src/Utils/castArray.js";

export type ParserAugmentor = (parser: MutableParser) => void;

Expand All @@ -73,7 +74,10 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme
return new ExposeNodeParser(typeChecker, nodeParser, config.expose, config.jsDoc);
}
function withTopRef(nodeParser: NodeParser): NodeParser {
return new TopRefNodeParser(chainNodeParser, config.type, config.topRef);
const typeArr = castArray(config.type);
// If we have multiple types, don't set a top-level $ref.
const topRefFullName = typeArr && typeArr.length === 1 ? typeArr[0] : undefined;
return new TopRefNodeParser(chainNodeParser, topRefFullName, config.topRef);
}
function withJsDoc(nodeParser: SubNodeParser): SubNodeParser {
const extraTags = new Set(config.extraTags);
Expand Down
3 changes: 2 additions & 1 deletion src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ export interface Config {

/**
* Name of the type/interface to generate schema for.
* Can specify more than once to generate multiple schemas.
* Use "*" to generate schemas for all exported types.
*/
type?: string;
type?: string | string[];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not positive this is the right way to type this.

Copy link
Member

Choose a reason for hiding this comment

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

Looks reasonable

Copy link
Collaborator

Choose a reason for hiding this comment

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

Because of how you set a (value: string, previous: string[] | undefined) mapper, wouldn't type always be an array? Most likely an array with a single item inside?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If so, i think you can simplify the code by removing the castArray and letting all related types be a single array[] (Also remove undefined support and just check for types.length === 0.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If not, cast it into an array at the createGenerator(config).createSchema(config.type) level.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wonder if I'm misunderstanding what you guys are asking.

I thought that this Config object was defining valid inputs for the library, so that by type?: string | string[], we were saying that programmatic usage accepts undefined, a single string, or an array of string.

And I thought the (value: string, previous: string[] | undefined) mapper was only applying when invoked via the CLI. If --type is not passed on the CLI, the final value is undefined (b/c the mapper is never invoked). If one or more values are passed, then mapper is invoked and it will always be an array, even with just a single item.

So I think if Config.type was changed to an array, I think it would be a breaking change.

I have a PR making this change here against an older version of this branch:

https://github.com/srsudar/ts-json-schema-generator/pull/1/files#diff-b83e1482492860acb73eb6a72535d0dd31b4fc971525ff13c35fb97222172d39

Am I misunderstanding something? Is that version basically the change you're asking for? Happy to do it if so!

Copy link
Member

Choose a reason for hiding this comment

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

I think supporting both and just using the default is easier. I'll make the change.


/**
* Minify the output JSON schema (no whitespace).
Expand Down
13 changes: 8 additions & 5 deletions src/SchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { TypeFormatter } from "./TypeFormatter.js";
import type { StringMap } from "./Utils/StringMap.js";
import { hasJsDocTag } from "./Utils/hasJsDocTag.js";
import { removeUnreachable } from "./Utils/removeUnreachable.js";
import { castArray } from "./Utils/castArray.js";
import { symbolAtNode } from "./Utils/symbolAtNode.js";

export class SchemaGenerator {
Expand All @@ -20,8 +21,8 @@ export class SchemaGenerator {
protected readonly config?: Config,
) {}

public createSchema(fullName?: string): Schema {
const rootNodes = this.getRootNodes(fullName);
public createSchema(fullNames?: string | string[]): Schema {
const rootNodes = this.getRootNodes(castArray(fullNames));
return this.createSchemaFromNodes(rootNodes);
}

Expand Down Expand Up @@ -60,9 +61,11 @@ export class SchemaGenerator {
};
}

protected getRootNodes(fullName: string | undefined): ts.Node[] {
if (fullName && fullName !== "*") {
return [this.findNamedNode(fullName)];
protected getRootNodes(fullNames: string[] | undefined): ts.Node[] {
// ["*"] means generate everything.
const generateAll = !fullNames || (fullNames && fullNames.length === 1 && fullNames[0] === "*");
if (!generateAll) {
return fullNames.map((name) => this.findNamedNode(name));
}

const rootFileNames = this.program.getRootFileNames();
Expand Down
7 changes: 7 additions & 0 deletions src/Utils/castArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function castArray<T>(input: undefined | T | T[]): undefined | T[] {
if (input === undefined) {
return undefined;
}

return Array.isArray(input) ? input : [input];
}
19 changes: 17 additions & 2 deletions test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import { EnumType } from "../src/Type/EnumType.js";
import { FunctionType } from "../src/Type/FunctionType.js";
import { StringType } from "../src/Type/StringType.js";
import type { TypeFormatter } from "../src/TypeFormatter.js";
import { castArray } from "../src/Utils/castArray.js";
import { uniqueArray } from "../src/Utils/uniqueArray.js";

const basePath = "test/config";

function assertSchema(
name: string,
userConfig: Config & { type: string },
userConfig: Config & { type: string | string[] },
tsconfig?: boolean,
formatterAugmentor?: FormatterAugmentor,
parserAugmentor?: ParserAugmentor,
Expand All @@ -51,7 +52,7 @@ function assertSchema(
config,
);

const schema = generator.createSchema(config.type);
const schema = generator.createSchema(castArray(config.type));
const schemaFile = resolve(`${basePath}/${name}/schema.json`);

if (process.env.UPDATE_SCHEMA) {
Expand Down Expand Up @@ -390,6 +391,20 @@ describe("config", () => {
}),
);

it(
"multiple-types",
assertSchema("multiple-types", {
type: ["MyObject1", "MyObject2"],
}),
);

it(
"multiple-types-all",
assertSchema("multiple-types-all", {
type: ["MyObject1", "MyObject2", "Object1Prop", "Object2Prop"],
}),
);

it(
"mapped-intersection",
assertSchema("mapped-intersection", {
Expand Down
30 changes: 30 additions & 0 deletions test/config/multiple-types-all/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type NonExportedType = {
misc: number;
};

export type ExportedType = {
val: string;
val2: NonExportedType;
};

export interface ExportedInterface {
val: string;
}

export type Object1Prop = {
name: string;
};

export type Object2Prop = {
description: string;
};

export type MyObject1 = {
id: number;
bar: Object1Prop;
};

export type MyObject2 = {
idStr: string;
baz: Object2Prop;
};
61 changes: 61 additions & 0 deletions test/config/multiple-types-all/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MyObject1": {
"properties": {
"id": {
"type": "number"
},
"bar": {
"$ref": "#/definitions/Object1Prop"
}
},
"required": [
"id",
"bar"
],
"type": "object",
"additionalProperties": false
},
"MyObject2": {
"properties": {
"idStr": {
"type": "string"
},
"baz": {
"$ref": "#/definitions/Object2Prop"
}
},
"required": [
"idStr",
"baz"
],
"type": "object",
"additionalProperties": false
},
"Object1Prop": {
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"type": "object",
"additionalProperties": false
},
"Object2Prop": {
"properties": {
"description": {
"type": "string"
}
},
"required": [
"description"
],
"type": "object",
"additionalProperties": false
}
}
}
31 changes: 31 additions & 0 deletions test/config/multiple-types/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
type NonExportedType = {
misc: number;
};

export type ExportedType = {
val: string;
val2: NonExportedType;
};

export interface ExportedInterface {
val: string;
}

// Exported, so we include it as a root node
export type Object1Prop = {
name: string;
};

type Object2Prop = {
description: string;
};

export type MyObject1 = {
id: number;
bar: Object1Prop;
};

export type MyObject2 = {
idStr: string;
baz: Object2Prop;
};
58 changes: 58 additions & 0 deletions test/config/multiple-types/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MyObject1": {
"properties": {
"id": {
"type": "number"
},
"bar": {
"$ref": "#/definitions/Object1Prop"
}
},
"required": [
"id",
"bar"
],
"type": "object",
"additionalProperties": false
},
"MyObject2": {
"properties": {
"idStr": {
"type": "string"
},
"baz": {
"properties": {
"description": {
"type": "string"
}
},
"required": [
"description"
],
"type": "object",
"additionalProperties": false
}
},
"required": [
"idStr",
"baz"
],
"type": "object",
"additionalProperties": false
},
"Object1Prop": {
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"type": "object",
"additionalProperties": false
}
}
}
14 changes: 12 additions & 2 deletions ts-json-schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@ import type { Config } from "./src/Config.js";
import { BaseError } from "./src/Error/BaseError.js";

import pkg from "./package.json";
import { castArray } from "./src/Utils/castArray.js";

const args = new Command()
.option("-p, --path <path>", "Source file path")
.option("-t, --type <name>", "Type name")
.option(
"-t, --type <name>",
"Type name (can be passed multiple times)",
(value: string, previous: string[] | undefined) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

if it will always return an array, then the typings can be a simple string[]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My thinking here was that previous can be undefined, since I'm not passing a default arg.

Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we use ... instead as documented in https://github.com/tj/commander.js?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding (and this is just from reading the docs and talking to chatgpt--I don't have any real experience with commander), was that ... allows for --type Foo Bar rather than --type Foo --type Bar, as Arthur asked for (and that I guess is more common, at least according to the LLMs). There were also some parsing ambiguities around variadic arguments that seemed to trip people up when I googled.

Although I see now that one of their examples does show both working:

$ collect --letter -n 1 -n 2 3 -- operand
Options:  { number: [ '1', '2', '3' ], letter: true }
Remaining arguments:  [ 'operand' ]

They have a section in the docs about ambiguity in parsing variadic arguments that I didn't think applied to the collect approach I took here.

Let me know if you'd prefer I switch to variadic arguments.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like his approach of always returning an array :)

if (previous) {
return previous.concat(value);
}
return [value];
},
)
.option("-i, --id <name>", "$id for generated schema")
.option("-f, --tsconfig <path>", "Custom tsconfig.json path")
.addOption(
Expand Down Expand Up @@ -84,7 +94,7 @@ const config: Config = {
};

try {
const schema = createGenerator(config).createSchema(args.type);
const schema = createGenerator(config).createSchema(castArray(args.type));

const stringify = config.sortProps ? stableStringify : JSON.stringify;
// need as string since TS can't figure out that the string | undefined case doesn't happen
Expand Down
Loading