Skip to content

Commit 9350900

Browse files
committed
Revise schema-to-json for bundle safety and tests
The package @trigger.dev/schema-to-json has been revised to ensure bundle safety by removing direct dependencies on schema libraries such as Zod, Yup, and Effect. This change minimizes bundle size and enhances tree-shaking by allowing external conversion libraries to be utilized only at runtime if necessary. As a result, the README was updated to reflect this usage pattern. - Introduced `initializeSchemaConverters` function to load necessary conversion libraries at runtime, keeping the base package slim. - Adjusted test suite to initialize converters before tests, ensuring accurate testing of schema conversion capabilities. - Updated `schemaToJsonSchema` function to dynamically check for availability of conversion libraries, improving flexibility without increasing the package size. - Added configuration files for Vitest to support the new testing framework, reflecting the transition from previous test setups. These enhancements ensure that only the schema libraries actively used in an application are bundled, optimizing performance and resource usage.
1 parent a86dd5e commit 9350900

File tree

6 files changed

+210
-78
lines changed

6 files changed

+210
-78
lines changed

packages/schema-to-json/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
dist
3+
.tshy
4+
.tshy-build
5+
*.log
6+
.DS_Store

packages/schema-to-json/README.md

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,61 @@ Convert various schema validation libraries to JSON Schema format.
88
npm install @trigger.dev/schema-to-json
99
```
1010

11+
## Important: Bundle Safety
12+
13+
This package is designed to be **bundle-safe**. It does NOT bundle any schema libraries (zod, yup, etc.) as dependencies. Instead:
14+
15+
1. **Built-in conversions** work immediately (ArkType, Zod 4, TypeBox)
16+
2. **External conversions** (Zod 3, Yup, Effect) require the conversion libraries to be available at runtime
17+
18+
This design ensures that:
19+
- ✅ Your bundle size stays small
20+
- ✅ You only include the schema libraries you actually use
21+
- ✅ Tree-shaking works properly
22+
- ✅ No unnecessary dependencies are installed
23+
1124
## Supported Schema Libraries
1225

1326
-**Zod** - Full support
14-
- Zod 4: Native support via built-in `toJsonSchema` method
15-
- Zod 3: Support via `zod-to-json-schema` library
16-
-**Yup** - Full support via `@sodaru/yup-to-json-schema`
27+
- Zod 4: Native support via built-in `toJsonSchema` method (no external deps needed)
28+
- Zod 3: Requires `zod-to-json-schema` to be installed
29+
-**Yup** - Requires `@sodaru/yup-to-json-schema` to be installed
1730
-**ArkType** - Native support (built-in `toJsonSchema` method)
18-
-**Effect/Schema** - Full support via Effect's JSONSchema module
31+
-**Effect/Schema** - Requires `effect` or `@effect/schema` to be installed
1932
-**TypeBox** - Native support (already JSON Schema compliant)
2033
-**Valibot** - Coming soon
2134
-**Superstruct** - Coming soon
2235
-**Runtypes** - Coming soon
2336

2437
## Usage
2538

39+
### Basic Usage (Built-in conversions only)
40+
2641
```typescript
2742
import { schemaToJsonSchema } from '@trigger.dev/schema-to-json';
43+
import { type } from 'arktype';
44+
45+
// Works immediately for schemas with built-in conversion
46+
const arkSchema = type({
47+
name: 'string',
48+
age: 'number',
49+
});
50+
51+
const result = schemaToJsonSchema(arkSchema);
52+
console.log(result);
53+
// { jsonSchema: {...}, schemaType: 'arktype' }
54+
```
55+
56+
### Full Usage (With external conversion libraries)
57+
58+
```typescript
59+
import { schemaToJsonSchema, initializeSchemaConverters } from '@trigger.dev/schema-to-json';
2860
import { z } from 'zod';
2961

30-
// Convert a Zod schema
62+
// Initialize converters once in your app (loads conversion libraries if available)
63+
await initializeSchemaConverters();
64+
65+
// Now you can convert Zod 3, Yup, and Effect schemas
3166
const zodSchema = z.object({
3267
name: z.string(),
3368
age: z.number(),
@@ -66,6 +101,12 @@ Convert a schema to JSON Schema format.
66101
- `{ jsonSchema, schemaType }` - The converted JSON Schema and detected type
67102
- `undefined` - If the schema cannot be converted
68103

104+
### `initializeSchemaConverters()`
105+
106+
Initialize the external conversion libraries. Call this once in your application if you need to convert schemas that don't have built-in JSON Schema support (Zod 3, Yup, Effect).
107+
108+
**Returns:** `Promise<void>`
109+
69110
### `canConvertSchema(schema)`
70111

71112
Check if a schema can be converted to JSON Schema.
@@ -78,6 +119,12 @@ Detect the type of schema.
78119

79120
**Returns:** `'zod' | 'yup' | 'arktype' | 'effect' | 'valibot' | 'superstruct' | 'runtypes' | 'typebox' | 'unknown'`
80121

122+
### `areConvertersInitialized()`
123+
124+
Check which conversion libraries are available.
125+
126+
**Returns:** `{ zod: boolean, yup: boolean, effect: boolean }`
127+
81128
## Peer Dependencies
82129

83130
Each schema library is an optional peer dependency. Install only the ones you need:

packages/schema-to-json/src/__tests__/index.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
1-
import { describe, it, expect } from 'vitest';
1+
import { describe, it, expect, beforeAll } from 'vitest';
22
import { z } from 'zod';
33
import * as y from 'yup';
44
import { type } from 'arktype';
55
import { Schema } from '@effect/schema';
66
import { Type } from '@sinclair/typebox';
7-
import { schemaToJsonSchema, canConvertSchema, detectSchemaType } from '../index.js';
7+
import {
8+
schemaToJsonSchema,
9+
canConvertSchema,
10+
detectSchemaType,
11+
initializeSchemaConverters,
12+
areConvertersInitialized
13+
} from '../index.js';
14+
15+
// Initialize converters before running tests
16+
beforeAll(async () => {
17+
await initializeSchemaConverters();
18+
});
819

920
describe('schemaToJsonSchema', () => {
21+
describe('Initialization', () => {
22+
it('should have converters initialized', () => {
23+
const status = areConvertersInitialized();
24+
expect(status.zod).toBe(true);
25+
expect(status.yup).toBe(true);
26+
expect(status.effect).toBe(true);
27+
});
28+
});
29+
1030
describe('Zod schemas', () => {
1131
it('should convert a simple Zod object schema', () => {
1232
const schema = z.object({

packages/schema-to-json/src/index.ts

Lines changed: 114 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,32 @@ export interface ConversionResult {
2424

2525
/**
2626
* Convert a schema from various validation libraries to JSON Schema
27+
*
28+
* This function attempts to convert schemas without requiring external dependencies to be bundled.
29+
* It will only succeed if:
30+
* 1. The schema has built-in JSON Schema conversion (ArkType, Zod 4, TypeBox)
31+
* 2. The required conversion library is available at runtime (zod-to-json-schema, @sodaru/yup-to-json-schema, etc.)
32+
*
33+
* @param schema The schema to convert
34+
* @param options Optional conversion options
35+
* @returns The conversion result or undefined if conversion is not possible
2736
*/
2837
export function schemaToJsonSchema(schema: Schema, options?: ConversionOptions): ConversionResult | undefined {
2938
const parser = schema as any;
3039

31-
// Check if schema has a built-in toJsonSchema method (e.g., ArkType)
40+
// Check if schema has a built-in toJsonSchema method (e.g., ArkType, Zod 4)
3241
if (typeof parser.toJsonSchema === "function") {
33-
const jsonSchema = parser.toJsonSchema();
34-
return {
35-
jsonSchema: options?.additionalProperties ? { ...jsonSchema, ...options.additionalProperties } : jsonSchema,
36-
schemaType: 'arktype'
37-
};
42+
try {
43+
const jsonSchema = parser.toJsonSchema();
44+
// Determine if it's Zod or ArkType based on other methods
45+
const schemaType = (typeof parser.parseAsync === "function" || typeof parser.parse === "function") ? 'zod' : 'arktype';
46+
return {
47+
jsonSchema: options?.additionalProperties ? { ...jsonSchema, ...options.additionalProperties } : jsonSchema,
48+
schemaType
49+
};
50+
} catch (error) {
51+
// If toJsonSchema fails, continue to other checks
52+
}
3853
}
3954

4055
// Check if it's a TypeBox schema (has Static and Kind symbols)
@@ -46,106 +61,116 @@ export function schemaToJsonSchema(schema: Schema, options?: ConversionOptions):
4661
};
4762
}
4863

49-
// Check if it's a Zod schema
64+
// For schemas that need external libraries, we need to check if they're available
65+
// This approach avoids bundling the dependencies while still allowing runtime usage
66+
67+
// Check if it's a Zod schema (without built-in toJsonSchema)
5068
if (typeof parser.parseAsync === "function" || typeof parser.parse === "function") {
51-
// Check if it's Zod 4 with built-in toJsonSchema method
52-
if (typeof parser.toJsonSchema === "function") {
53-
try {
54-
const jsonSchema = parser.toJsonSchema();
55-
return {
56-
jsonSchema: options?.additionalProperties ? { ...jsonSchema, ...options.additionalProperties } : jsonSchema,
57-
schemaType: 'zod'
58-
};
59-
} catch (error) {
60-
console.warn('Failed to convert Zod 4 schema using built-in toJsonSchema:', error);
61-
}
62-
}
63-
64-
// Fall back to zod-to-json-schema library (for Zod 3 or if built-in method fails)
6569
try {
66-
const { zodToJsonSchema } = require('zod-to-json-schema');
67-
const jsonSchema = options?.name
68-
? zodToJsonSchema(parser, options.name)
69-
: zodToJsonSchema(parser);
70-
71-
if (jsonSchema && typeof jsonSchema === 'object' && '$schema' in jsonSchema) {
72-
// Remove the $schema property as it's not needed for our use case
73-
const { $schema, ...rest } = jsonSchema as any;
70+
// Try to access zod-to-json-schema if it's available
71+
// @ts-ignore - This is intentionally dynamic
72+
if (typeof globalThis.__zodToJsonSchema !== 'undefined') {
73+
// @ts-ignore
74+
const { zodToJsonSchema } = globalThis.__zodToJsonSchema;
75+
const jsonSchema = options?.name
76+
? zodToJsonSchema(parser, options.name)
77+
: zodToJsonSchema(parser);
78+
79+
if (jsonSchema && typeof jsonSchema === 'object' && '$schema' in jsonSchema) {
80+
const { $schema, ...rest } = jsonSchema as any;
81+
return {
82+
jsonSchema: options?.additionalProperties ? { ...rest, ...options.additionalProperties } : rest,
83+
schemaType: 'zod'
84+
};
85+
}
86+
7487
return {
75-
jsonSchema: options?.additionalProperties ? { ...rest, ...options.additionalProperties } : rest,
88+
jsonSchema: options?.additionalProperties ? { ...jsonSchema, ...options.additionalProperties } : jsonSchema,
7689
schemaType: 'zod'
7790
};
7891
}
79-
80-
return {
81-
jsonSchema: options?.additionalProperties ? { ...jsonSchema, ...options.additionalProperties } : jsonSchema,
82-
schemaType: 'zod'
83-
};
8492
} catch (error) {
85-
console.warn('Failed to convert Zod schema to JSON Schema using zod-to-json-schema:', error);
86-
return undefined;
93+
// Library not available
8794
}
8895
}
8996

9097
// Check if it's a Yup schema
9198
if (typeof parser.validateSync === "function" && typeof parser.describe === "function") {
9299
try {
93-
const { convertSchema } = require('@sodaru/yup-to-json-schema');
94-
const jsonSchema = convertSchema(parser);
95-
return {
96-
jsonSchema: options?.additionalProperties ? { ...jsonSchema, ...options.additionalProperties } : jsonSchema,
97-
schemaType: 'yup'
98-
};
100+
// @ts-ignore
101+
if (typeof globalThis.__yupToJsonSchema !== 'undefined') {
102+
// @ts-ignore
103+
const { convertSchema } = globalThis.__yupToJsonSchema;
104+
const jsonSchema = convertSchema(parser);
105+
return {
106+
jsonSchema: options?.additionalProperties ? { ...jsonSchema, ...options.additionalProperties } : jsonSchema,
107+
schemaType: 'yup'
108+
};
109+
}
99110
} catch (error) {
100-
console.warn('Failed to convert Yup schema to JSON Schema:', error);
101-
return undefined;
111+
// Library not available
102112
}
103113
}
104114

105115
// Check if it's an Effect schema
106116
if (parser._tag === "Schema" || parser._tag === "SchemaClass" || typeof parser.ast === "function") {
107117
try {
108-
// Try to load Effect's JSONSchema module
109-
const effectModule = require('effect');
110-
const schemaModule = require('@effect/schema');
111-
112-
if (effectModule?.JSONSchema && schemaModule?.JSONSchema) {
113-
const JSONSchema = schemaModule.JSONSchema || effectModule.JSONSchema;
118+
// @ts-ignore
119+
if (typeof globalThis.__effectJsonSchema !== 'undefined') {
120+
// @ts-ignore
121+
const { JSONSchema } = globalThis.__effectJsonSchema;
114122
const jsonSchema = JSONSchema.make(parser);
115123
return {
116124
jsonSchema: options?.additionalProperties ? { ...jsonSchema, ...options.additionalProperties } : jsonSchema,
117125
schemaType: 'effect'
118126
};
119127
}
120128
} catch (error) {
121-
console.warn('Failed to convert Effect schema to JSON Schema:', error);
122-
return undefined;
129+
// Library not available
123130
}
124131
}
125132

126-
// Check if it's a Valibot schema
127-
if (typeof parser === "function" && parser._def?.kind !== undefined) {
128-
// Valibot doesn't have built-in JSON Schema conversion yet
129-
// We could implement a basic converter for common types
130-
return undefined;
131-
}
133+
// Future schema types can be added here...
132134

133-
// Check if it's a Superstruct schema
134-
if (typeof parser.create === "function" && parser.TYPE !== undefined) {
135-
// Superstruct doesn't have built-in JSON Schema conversion
136-
// We could implement a basic converter for common types
137-
return undefined;
135+
// Unknown schema type
136+
return undefined;
137+
}
138+
139+
/**
140+
* Initialize the schema conversion libraries
141+
* This should be called by the consuming application if they want to enable
142+
* conversion for schemas that don't have built-in JSON Schema support
143+
*/
144+
export async function initializeSchemaConverters(): Promise<void> {
145+
try {
146+
// @ts-ignore
147+
globalThis.__zodToJsonSchema = await import('zod-to-json-schema');
148+
} catch {
149+
// Zod conversion not available
138150
}
139151

140-
// Check if it's a Runtypes schema
141-
if (typeof parser.guard === "function" && parser._tag !== undefined) {
142-
// Runtypes doesn't have built-in JSON Schema conversion
143-
// We could implement a basic converter for common types
144-
return undefined;
152+
try {
153+
// @ts-ignore
154+
globalThis.__yupToJsonSchema = await import('@sodaru/yup-to-json-schema');
155+
} catch {
156+
// Yup conversion not available
145157
}
146158

147-
// Unknown schema type
148-
return undefined;
159+
try {
160+
// Try Effect first, then @effect/schema
161+
let module;
162+
try {
163+
module = await import('effect');
164+
} catch {
165+
module = await import('@effect/schema');
166+
}
167+
if (module?.JSONSchema) {
168+
// @ts-ignore
169+
globalThis.__effectJsonSchema = { JSONSchema: module.JSONSchema };
170+
}
171+
} catch {
172+
// Effect conversion not available
173+
}
149174
}
150175

151176
/**
@@ -162,4 +187,22 @@ export function canConvertSchema(schema: Schema): boolean {
162187
export function detectSchemaType(schema: Schema): ConversionResult['schemaType'] {
163188
const result = schemaToJsonSchema(schema);
164189
return result?.schemaType ?? 'unknown';
190+
}
191+
192+
/**
193+
* Check if the conversion libraries are initialized
194+
*/
195+
export function areConvertersInitialized(): {
196+
zod: boolean;
197+
yup: boolean;
198+
effect: boolean;
199+
} {
200+
return {
201+
// @ts-ignore
202+
zod: typeof globalThis.__zodToJsonSchema !== 'undefined',
203+
// @ts-ignore
204+
yup: typeof globalThis.__yupToJsonSchema !== 'undefined',
205+
// @ts-ignore
206+
effect: typeof globalThis.__effectJsonSchema !== 'undefined',
207+
};
165208
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../../.configs/tsconfig.base.json",
3+
"compilerOptions": {
4+
"rootDir": "./",
5+
"types": ["node", "vitest/globals"]
6+
},
7+
"include": ["./src/**/*.test.ts", "./test/**/*.ts"]
8+
}

0 commit comments

Comments
 (0)