Repository: fabien0102/ts-to-zod (ts)

Issue:
Support JSDoc @description tags
## Feature description

It would be fantastic to support JSDoc `@description` tags, mapped to Zod's `.describe()`

### Input

```ts

/**
 * @description Extract information about a single entity
 */
export interface Properties {
  /**
   * @description The name of this entity
   */
  name: string;
  location: string;
}
```

### Output

```ts
// Generated by ts-to-zod
import { z } from "zod";

export const propertiesSchema = z.object({
  name: z.string().describe("The name of this entity"),
  location: z.string(),
}).describe("Extract information about a single entity")
```


Pull request description:
Add JSDoc @description support
# Why

Fixes #193 — now JSDoc `@description` tags are applied to the Zod schema via `.describe()`

Helps for when Zod schemas are flowing further downstream into JSONSchemas and such :)

Patch:
diff --git a/README.md b/README.md
index 5d15bdc..5b33998 100644
--- a/README.md
+++ b/README.md
@@ -147,33 +147,34 @@ export const heroContactSchema = z.object({
 
 Other JSDoc tags are available:
 
-| JSDoc keyword      | JSDoc Example | Description                               | Generated Zod            |
-| ------------------ | ------------- | ----------------------------------------- | ------------------------ |
-| `@default {value}` | `@default 42` | Sets a default value for the property     | `z.number().default(42)` |
-| `@strict`          | `@strict`     | Adds the `strict()` modifier to an object | `z.object().strict()`    |
+| JSDoc keyword          | JSDoc Example            | Description                               | Generated Zod                      |
+| ---------------------- | ------------------------ | ----------------------------------------- | ---------------------------------- |
+| `@description {value}` | `@description Full name` | Sets the description of the property      | `z.string().describe("Full name")` |
+| `@default {value}`     | `@default 42`            | Sets a default value for the property     | `z.number().default(42)`           |
+| `@strict`              | `@strict`                | Adds the `strict()` modifier to an object | `z.object().strict()`              |
 
 ## JSDoc tags for elements of `string` and `number` arrays
 
 Elements of `string` and `number` arrays can be validated using the following JSDoc tags (for details see above).
 
-| JSDoc keyword                          |
-| ---------------------------------------|
-| `@elementMinimum {number} [err_msg]`   |
-| `@elementMaximum {number} [err_msg]`   |
-| `@elementMinLength {number} [err_msg]` |
-| `@elementMaxLength {number} [err_msg]` |
-| `@elementFormat {FormatType} [err_msg]`|
-| `@elementPattern {regex}`              |
+| JSDoc keyword                           |
+| --------------------------------------- |
+| `@elementDescription {value}`           |
+| `@elementMinimum {number} [err_msg]`    |
+| `@elementMaximum {number} [err_msg]`    |
+| `@elementMinLength {number} [err_msg]`  |
+| `@elementMaxLength {number} [err_msg]`  |
+| `@elementFormat {FormatType} [err_msg]` |
+| `@elementPattern {regex}`               |
 
 Example:
 
 ```ts
 // source.ts
 export interface EnemyContact {
-
   /**
    * The names of the enemy.
-   * 
+   *
    * @elementMinLength 5
    * @elementMaxLength 10
    * @minLength 2
@@ -184,6 +185,7 @@ export interface EnemyContact {
   /**
    * The phone numbers of the enemy.
    *
+   * @description Include home and work numbers
    * @elementPattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
    */
   phoneNumbers: string[];
@@ -206,7 +208,9 @@ export const enemyContactSchema = z.object({
    *
    * @elementPattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
    */
-  phoneNumbers: z.array(z.string().regex(/^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$/)),
+  phoneNumbers: z
+    .array(z.string().regex(/^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$/))
+    .describe("Include home and work numbers"),
 });
 ```
 
diff --git a/src/core/generateZodSchema.ts b/src/core/generateZodSchema.ts
index 6de0483..80645a1 100644
--- a/src/core/generateZodSchema.ts
+++ b/src/core/generateZodSchema.ts
@@ -697,6 +697,7 @@ function buildZodPrimitive({
           typeNode: typeNode.elementType,
           isOptional: false,
           jsDocTags: {
+            description: jsDocTags.elementDescription,
             minimum: jsDocTags.elementMinimum,
             maximum: jsDocTags.elementMaximum,
             minLength: jsDocTags.elementMinLength,
diff --git a/src/core/jsDocTags.ts b/src/core/jsDocTags.ts
index 176461b..cbc8070 100644
--- a/src/core/jsDocTags.ts
+++ b/src/core/jsDocTags.ts
@@ -53,6 +53,7 @@ type TagWithError<T> = {
  * JSDoc special tags that can be converted in zod flags.
  */
 export interface JSDocTagsBase {
+  description?: string;
   minimum?: TagWithError<number>;
   maximum?: TagWithError<number>;
   default?: number | string | boolean | null;
@@ -69,7 +70,13 @@ export interface JSDocTagsBase {
 
 export type ElementJSDocTags = Pick<
   JSDocTagsBase,
-  "minimum" | "maximum" | "minLength" | "maxLength" | "pattern" | "format"
+  | "description"
+  | "minimum"
+  | "maximum"
+  | "minLength"
+  | "maxLength"
+  | "pattern"
+  | "format"
 >;
 
 export type JSDocTags = JSDocTagsBase & {
@@ -77,6 +84,7 @@ export type JSDocTags = JSDocTagsBase & {
 };
 
 const jsDocTagKeys: Array<keyof JSDocTags> = [
+  "description",
   "minimum",
   "maximum",
   "default",
@@ -84,6 +92,7 @@ const jsDocTagKeys: Array<keyof JSDocTags> = [
   "maxLength",
   "format",
   "pattern",
+  "elementDescription",
   "elementMinimum",
   "elementMaximum",
   "elementMinLength",
@@ -164,6 +173,8 @@ export function getJSDocTags(nodeType: ts.Node, sourceFile: ts.SourceFile) {
               jsDocTags[tagName] = { value: parseInt(value), errorMessage };
             }
             break;
+          case "description":
+          case "elementDescription":
           case "pattern":
           case "elementPattern":
             if (tag.comment) {
@@ -199,6 +210,10 @@ export function getJSDocTags(nodeType: ts.Node, sourceFile: ts.SourceFile) {
               jsDocTags[tagName] = tag.comment;
             }
             break;
+          case "strict":
+            break;
+          default:
+            tagName satisfies never;
         }
       });
     });
@@ -306,6 +321,13 @@ export function jsDocTagToZodProperties(
       identifier: "required",
     });
   }
+  if (jsDocTags.description !== undefined) {
+    zodProperties.push({
+      identifier: "describe",
+      expressions: [f.createStringLiteral(jsDocTags.description)],
+    });
+  }
+
   if (jsDocTags.default !== undefined) {
     zodProperties.push({
       identifier: "default",


Test patch:
diff --git a/src/core/generateZodSchema.test.ts b/src/core/generateZodSchema.test.ts
index 9860701..512ee53 100644
--- a/src/core/generateZodSchema.test.ts
+++ b/src/core/generateZodSchema.test.ts
@@ -1114,6 +1114,77 @@ describe("generateZodSchema", () => {
     `);
   });
 
+  it("should add describe() when @description is used (top-level)", () => {
+    const source = `/**
+    * @description Originally Superman could leap, but not fly.
+    */
+    export type Superman = {
+      name: "superman";
+      weakness: Kryptonite;
+      age: number;
+      enemies: Array<string>;
+    };`;
+    expect(generate(source)).toMatchInlineSnapshot(`
+       "/**
+           * @description Originally Superman could leap, but not fly.
+           */
+       export const supermanSchema = z.object({
+           name: z.literal("superman"),
+           weakness: kryptoniteSchema,
+           age: z.number(),
+           enemies: z.array(z.string())
+       }).describe("Originally Superman could leap, but not fly.");"
+     `);
+  });
+
+  it("should add describe() when @description is used (property-level)", () => {
+    const source = `
+    export type Superman = {
+      name: "superman";
+      weakness: Kryptonite;
+      age: number;
+      /**
+        * @description Lex Luthor, Branaic, etc.
+        */
+      enemies: Array<string>;
+    };`;
+    expect(generate(source)).toMatchInlineSnapshot(`
+       "export const supermanSchema = z.object({
+           name: z.literal("superman"),
+           weakness: kryptoniteSchema,
+           age: z.number(),
+           /**
+             * @description Lex Luthor, Branaic, etc.
+             */
+           enemies: z.array(z.string()).describe("Lex Luthor, Branaic, etc.")
+       });"
+     `);
+  });
+
+  it("should add describe() when @description is used (array elements)", () => {
+    const source = `
+    export type Superman = {
+      name: "superman";
+      weakness: Kryptonite;
+      age: number;
+      /**
+        * @elementDescription Name of an enemy
+        */
+      enemies: Array<string>;
+    };`;
+    expect(generate(source)).toMatchInlineSnapshot(`
+       "export const supermanSchema = z.object({
+           name: z.literal("superman"),
+           weakness: kryptoniteSchema,
+           age: z.number(),
+           /**
+             * @elementDescription Name of an enemy
+             */
+           enemies: z.array(z.string().describe("Name of an enemy"))
+       });"
+     `);
+  });
+
   it("should deal with nullable", () => {
     const source = `export interface A {
       /** @minimum 0 */