Skip to content

Commit

Permalink
feat: allow to append or override schema via jsdoc tag (#266)
Browse files Browse the repository at this point in the history
implementation taken from #125
  • Loading branch information
schiller-manuel authored Sep 3, 2024
1 parent 1346781 commit dc7eea8
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 19 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,13 @@ export const heroContactSchema = z.object({

Other JSDoc tags are available:

| 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 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()` |
| `@schema` | `@schema .catch('foo')` | If value starts with a `.`, appends the specified value to the generated schema. Otherwise this value will override the generated schema. | `z.string().catch('foo')` |
|

## JSDoc tags for elements of `string` and `number` arrays

Expand Down
42 changes: 42 additions & 0 deletions src/core/generateZodSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,48 @@ describe("generateZodSchema", () => {
`);
});

it("should append schema based on `schema` tag", () => {
const source = `export interface HeroContact {
/**
* The email of the hero.
*
* @schema .trim().catch('[email protected]')
*/
email: string;
}`;
expect(generate(source)).toMatchInlineSnapshot(`
"export const heroContactSchema = z.object({
/**
* The email of the hero.
*
* @schema .trim().catch('[email protected]')
*/
email: z.string().trim().catch('[email protected]')
});"
`);
});

it("should overrride schema based on `schema` tag", () => {
const source = `export interface HeroContact {
/**
* The email of the hero.
*
* @schema coerce.int()
*/
age: number;
}`;
expect(generate(source)).toMatchInlineSnapshot(`
"export const heroContactSchema = z.object({
/**
* The email of the hero.
*
* @schema coerce.int()
*/
age: z.coerce.int()
});"
`);
});

it("should generate custom error message for `format` tag", () => {
const source = `export interface HeroContact {
/**
Expand Down
62 changes: 48 additions & 14 deletions src/core/generateZodSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ type SchemaExtensionClause = {
omitOrPickKeys?: ts.TypeNode;
};

interface BuildZodPrimitiveParams {
z: string;
typeNode: ts.TypeNode;
isOptional: boolean;
isNullable?: boolean;
isPartial?: boolean;
isRequired?: boolean;
jsDocTags: JSDocTags;
customJSDocFormatTypes: CustomJSDocFormatTypes;
sourceFile: ts.SourceFile;
dependencies: string[];
getDependencyName: (identifierName: string) => string;
skipParseJSDoc: boolean;
}

/**
* Generate zod schema declaration
*
Expand Down Expand Up @@ -288,6 +303,35 @@ function buildZodProperties({
}

function buildZodPrimitive({
jsDocTags,
z,
...rest
}: BuildZodPrimitiveParams):
| ts.CallExpression
| ts.Identifier
| ts.PropertyAccessExpression {
const schema = jsDocTags.schema;
delete jsDocTags.schema;
const generatedSchema = buildZodPrimitiveInternal({ jsDocTags, z, ...rest });
// schema not specified? return generated one
if (!schema) {
return generatedSchema;
}
// schema starts with dot? append it
if (schema.startsWith(".")) {
return f.createPropertyAccessExpression(
generatedSchema,
f.createIdentifier(schema.slice(1))
);
}
// otherwise use provided schema verbatim
return f.createPropertyAccessExpression(
f.createIdentifier(z),
f.createIdentifier(schema)
);
}

function buildZodPrimitiveInternal({
z,
typeNode,
isOptional,
Expand All @@ -300,20 +344,10 @@ function buildZodPrimitive({
dependencies,
getDependencyName,
skipParseJSDoc,
}: {
z: string;
typeNode: ts.TypeNode;
isOptional: boolean;
isNullable?: boolean;
isPartial?: boolean;
isRequired?: boolean;
jsDocTags: JSDocTags;
customJSDocFormatTypes: CustomJSDocFormatTypes;
sourceFile: ts.SourceFile;
dependencies: string[];
getDependencyName: (identifierName: string) => string;
skipParseJSDoc: boolean;
}): ts.CallExpression | ts.Identifier | ts.PropertyAccessExpression {
}: BuildZodPrimitiveParams):
| ts.CallExpression
| ts.Identifier
| ts.PropertyAccessExpression {
const zodProperties = jsDocTagToZodProperties(
jsDocTags,
customJSDocFormatTypes,
Expand Down
3 changes: 3 additions & 0 deletions src/core/jsDocTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface JSDocTagsBase {
*/
pattern?: string;
strict?: boolean;
schema?: string;
}

export type ElementJSDocTags = Pick<
Expand All @@ -94,6 +95,7 @@ const jsDocTagKeys: Array<keyof JSDocTags> = [
"maxLength",
"format",
"pattern",
"schema",
"elementDescription",
"elementMinimum",
"elementMaximum",
Expand Down Expand Up @@ -177,6 +179,7 @@ export function getJSDocTags(nodeType: ts.Node, sourceFile: ts.SourceFile) {
break;
case "description":
case "elementDescription":
case "schema":
case "pattern":
case "elementPattern":
if (tag.comment) {
Expand Down

0 comments on commit dc7eea8

Please sign in to comment.