Skip to content

feat: Add --full-description option to include full comment in schema #2224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 11, 2025
Merged
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
43 changes: 22 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ Extended version of [https://github.com/xiag-ag/typescript-to-json-schema](https

Inspired by [`YousefED/typescript-json-schema`](https://github.com/YousefED/typescript-json-schema). Here's the differences list:

- this implementation avoids the use of `typeChecker.getTypeAtLocation()` (so probably it keeps correct type aliases)
- processing AST and formatting JSON schema have been split into two independent steps
- not exported types, interfaces, enums are not exposed in the `definitions` section in the JSON schema
- this implementation avoids the use of `typeChecker.getTypeAtLocation()` (so probably it keeps correct type aliases)
- processing AST and formatting JSON schema have been split into two independent steps
- not exported types, interfaces, enums are not exposed in the `definitions` section in the JSON schema

## Contributors

Expand Down Expand Up @@ -47,6 +47,7 @@ By default, the command-line generator will use the `tsconfig.json` file in the
-e, --expose <expose> Type exposing (choices: "all", "none", "export", default: "export")
-j, --jsDoc <extended> Read JsDoc annotations (choices: "none", "basic", "extended", default: "extended")
--markdown-description Generate `markdownDescription` in addition to `description`.
--full-description Include the full raw JSDoc comment as `fullDescription` in the schema.
--functions <functions> How to handle functions. `fail` will throw an error. `comment` will add a comment. `hide` will treat the function like a NeverType or HiddenType.
(choices: "fail", "comment", "hide", default: "comment")
--minify Minify generated schema (default: false)
Expand Down Expand Up @@ -221,20 +222,20 @@ fs.writeFile(outputPath, schemaString, (err) => {

## Current state

- `interface` types
- `enum` types
- `union`, `tuple`, `type[]` types
- `Date`, `RegExp`, `URL` types
- `string`, `boolean`, `number` types
- `"value"`, `123`, `true`, `false`, `null`, `undefined` literals
- type aliases
- generics
- `typeof`
- `keyof`
- conditional types
- functions
- `Promise<T>` unwraps to `T`
- Overrides (like `@format`)
- `interface` types
- `enum` types
- `union`, `tuple`, `type[]` types
- `Date`, `RegExp`, `URL` types
- `string`, `boolean`, `number` types
- `"value"`, `123`, `true`, `false`, `null`, `undefined` literals
- type aliases
- generics
- `typeof`
- `keyof`
- conditional types
- functions
- `Promise<T>` unwraps to `T`
- Overrides (like `@format`)

## Run locally

Expand All @@ -252,7 +253,7 @@ And connect via the debugger protocol.

Publishing is handled by a 2-branch [pre-release process](https://intuit.github.io/auto/docs/generated/shipit#next-branch-default), configured in `publish-auto.yml`. All changes should be based off the default `next` branch, and are published automatically.

- PRs made into the default branch are auto-deployed to the `next` pre-release tag on NPM. The result can be installed with `npm install ts-json-schema-generator@next`
- When merging into `next`, please use the `squash and merge` strategy.
- To release a new stable version, open a PR from `next` into `stable` using this [compare link](https://github.com/vega/ts-json-schema-generator/compare/stable...next).
- When merging from `next` into `stable`, please use the `create a merge commit` strategy.
- PRs made into the default branch are auto-deployed to the `next` pre-release tag on NPM. The result can be installed with `npm install ts-json-schema-generator@next`
- When merging into `next`, please use the `squash and merge` strategy.
- To release a new stable version, open a PR from `next` into `stable` using this [compare link](https://github.com/vega/ts-json-schema-generator/compare/stable...next).
- When merging from `next` into `stable`, please use the `create a merge commit` strategy.
7 changes: 6 additions & 1 deletion factory/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme
if (config.jsDoc === "extended") {
return new AnnotatedNodeParser(
nodeParser,
new ExtendedAnnotationsReader(typeChecker, extraTags, config.markdownDescription),
new ExtendedAnnotationsReader(
typeChecker,
extraTags,
config.markdownDescription,
config.fullDescription,
),
);
} else if (config.jsDoc === "basic") {
return new AnnotatedNodeParser(nodeParser, new BasicAnnotationsReader(extraTags));
Expand Down
35 changes: 25 additions & 10 deletions src/AnnotationsReader/ExtendedAnnotationsReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import json5 from "json5";
import type ts from "typescript";
import type { Annotations } from "../Type/AnnotatedType.js";
import { symbolAtNode } from "../Utils/symbolAtNode.js";
import { getFullDescription } from "../Utils/getFullDescription.js";
import { BasicAnnotationsReader } from "./BasicAnnotationsReader.js";

export class ExtendedAnnotationsReader extends BasicAnnotationsReader {
public constructor(
private typeChecker: ts.TypeChecker,
extraTags?: Set<string>,
private markdownDescription?: boolean,
private fullDescription?: boolean,
) {
super(extraTags);
}
Expand Down Expand Up @@ -44,21 +46,34 @@ export class ExtendedAnnotationsReader extends BasicAnnotationsReader {
return undefined;
}

const annotations: { description?: string; markdownDescription?: string; fullDescription?: string } = {};

const comments: ts.SymbolDisplayPart[] = symbol.getDocumentationComment(this.typeChecker);
if (!comments || !comments.length) {
return undefined;
}

const markdownDescription = comments
.map((comment) => comment.text)
.join(" ")
.replace(/\r/g, "")
.trim();
if (comments && comments.length) {
const markdownDescription = comments
.map((comment) => comment.text)
.join(" ")
.replace(/\r/g, "")
.trim();

const description = markdownDescription.replace(/(?<=[^\n])\n(?=[^\n*-])/g, " ").trim();
annotations.description = markdownDescription.replace(/(?<=[^\n])\n(?=[^\n*-])/g, " ").trim();

if (this.markdownDescription) {
annotations.markdownDescription = markdownDescription;
}
}

return this.markdownDescription ? { description, markdownDescription } : { description };
if (this.fullDescription) {
const fullDescription = getFullDescription(node)?.trim();
if (fullDescription) {
annotations.fullDescription = fullDescription;
}
}

return Object.keys(annotations).length ? annotations : undefined;
}

private getTypeAnnotation(node: ts.Node): Annotations | undefined {
const symbol = symbolAtNode(node);
if (!symbol) {
Expand Down
2 changes: 2 additions & 0 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Config {
topRef?: boolean;
jsDoc?: "none" | "extended" | "basic";
markdownDescription?: boolean;
fullDescription?: boolean;
sortProps?: boolean;
strictTuples?: boolean;
skipTypeCheck?: boolean;
Expand All @@ -27,6 +28,7 @@ export const DEFAULT_CONFIG: Omit<Required<Config>, "path" | "type" | "schemaId"
topRef: true,
jsDoc: "extended",
markdownDescription: false,
fullDescription: false,
sortProps: true,
strictTuples: false,
skipTypeCheck: false,
Expand Down
39 changes: 39 additions & 0 deletions src/Utils/getFullDescription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import ts from "typescript";

export function getFullDescription(node: ts.Node): string | undefined {
const sourceFile = node.getSourceFile();
const jsDocNodes = ts.getJSDocCommentsAndTags(node);

if (!jsDocNodes || jsDocNodes.length === 0) {
return undefined;
}

let rawText = "";

for (const jsDoc of jsDocNodes) {
rawText += jsDoc.getFullText(sourceFile) + "\n";
}

rawText = rawText.trim();

return getTextWithoutStars(rawText).trim();
}

function getTextWithoutStars(inputText: string) {
const innerTextWithStars = inputText.replace(/^\/\*\*[^\S\n]*\n?/, "").replace(/(\r?\n)?[^\S\n]*\*\/$/, "");

return innerTextWithStars
.split(/\n/)
.map((line) => {
const trimmedLine = line.trimStart();

if (trimmedLine[0] !== "*") {
return line;
}

const textStartPos = trimmedLine[1] === " " ? 2 : 1;

return trimmedLine.substring(textStartPos);
})
.join("\n");
}
18 changes: 17 additions & 1 deletion test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,14 @@ function assertSchema(
expect(typeof actual).toBe("object");
expect(actual).toEqual(expected);

const keywords: string[] = [];
if (config.markdownDescription) keywords.push("markdownDescription");
if (config.fullDescription) keywords.push("fullDescription");

const validator = new Ajv({
// skip full check if we are not encoding refs
validateFormats: config.encodeRefs === false ? undefined : true,
keywords: config.markdownDescription ? ["markdownDescription"] : undefined,
keywords: keywords.length ? keywords : undefined,
});

addFormats(validator);
Expand Down Expand Up @@ -341,6 +345,18 @@ describe("config", () => {
markdownDescription: true,
}),
);
it(
"full-description",
assertSchema("full-description", {
type: "MyObject",
expose: "export",
topRef: false,
jsDoc: "extended",
sortProps: true,
markdownDescription: true,
fullDescription: true,
}),
);
it(
"tsconfig-support",
assertSchema(
Expand Down
Loading
Loading