mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
feat: Add fork of json-schema-to-zod (no-changelog) (#11228)
This commit is contained in:
parent
c728a2ffe0
commit
86a94b5523
21
packages/@n8n/json-schema-to-zod/.eslintrc.js
Normal file
21
packages/@n8n/json-schema-to-zod/.eslintrc.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
const sharedOptions = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
ignorePatterns: ['jest.config.js'],
|
||||
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'@typescript-eslint/no-duplicate-imports': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
'n8n-local-rules/no-plain-errors': 'off',
|
||||
|
||||
complexity: 'error',
|
||||
},
|
||||
};
|
4
packages/@n8n/json-schema-to-zod/.gitignore
vendored
Normal file
4
packages/@n8n/json-schema-to-zod/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
coverage
|
||||
test/output
|
3
packages/@n8n/json-schema-to-zod/.npmignore
Normal file
3
packages/@n8n/json-schema-to-zod/.npmignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
src
|
||||
tsconfig*
|
||||
test
|
16
packages/@n8n/json-schema-to-zod/LICENSE
Normal file
16
packages/@n8n/json-schema-to-zod/LICENSE
Normal file
|
@ -0,0 +1,16 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2024, n8n
|
||||
Copyright (c) 2021, Stefan Terdell
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
34
packages/@n8n/json-schema-to-zod/README.md
Normal file
34
packages/@n8n/json-schema-to-zod/README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Json-Schema-to-Zod
|
||||
|
||||
A package to convert JSON schema (draft 4+) objects into Zod schemas in the form of Zod objects at runtime.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install @n8n/json-schema-to-zod
|
||||
```
|
||||
|
||||
### Simple example
|
||||
|
||||
```typescript
|
||||
import { jsonSchemaToZod } from "json-schema-to-zod";
|
||||
|
||||
const jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
hello: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const zodSchema = jsonSchemaToZod(myObject);
|
||||
```
|
||||
|
||||
### Overriding a parser
|
||||
|
||||
You can pass a function to the `overrideParser` option, which represents a function that receives the current schema node and the reference object, and should return a zod object when it wants to replace a default output. If the default output should be used for the node just return undefined.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This is a fork of [`json-schema-to-zod`](https://github.com/StefanTerdell/json-schema-to-zod).
|
5
packages/@n8n/json-schema-to-zod/jest.config.js
Normal file
5
packages/@n8n/json-schema-to-zod/jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...require('../../../jest.config'),
|
||||
setupFilesAfterEnv: ['<rootDir>/test/extend-expect.ts'],
|
||||
};
|
69
packages/@n8n/json-schema-to-zod/package.json
Normal file
69
packages/@n8n/json-schema-to-zod/package.json
Normal file
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"name": "@n8n/json-schema-to-zod",
|
||||
"version": "1.0.0",
|
||||
"description": "Converts JSON schema objects into Zod schemas",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"exports": {
|
||||
"import": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/cjs/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w",
|
||||
"format": "biome format --write src",
|
||||
"format:check": "biome ci src",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"build:types": "tsc -p tsconfig.types.json",
|
||||
"build:cjs": "tsc -p tsconfig.cjs.json && node postcjs.js",
|
||||
"build:esm": "tsc -p tsconfig.esm.json && node postesm.js",
|
||||
"build": "rimraf ./dist && pnpm run build:types && pnpm run build:cjs && pnpm run build:esm",
|
||||
"dry": "pnpm run build && pnpm pub --dry-run",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"keywords": [
|
||||
"zod",
|
||||
"json",
|
||||
"schema",
|
||||
"converter",
|
||||
"cli"
|
||||
],
|
||||
"author": "Stefan Terdell",
|
||||
"contributors": [
|
||||
"Chen (https://github.com/werifu)",
|
||||
"Nuno Carduso (https://github.com/ncardoso-barracuda)",
|
||||
"Lars Strojny (https://github.com/lstrojny)",
|
||||
"Navtoj Chahal (https://github.com/navtoj)",
|
||||
"Ben McCann (https://github.com/benmccann)",
|
||||
"Dmitry Zakharov (https://github.com/DZakh)",
|
||||
"Michel Turpin (https://github.com/grimly)",
|
||||
"David Barratt (https://github.com/davidbarratt)",
|
||||
"pevisscher (https://github.com/pevisscher)",
|
||||
"Aidin Abedi (https://github.com/aidinabedi)",
|
||||
"Brett Zamir (https://github.com/brettz9)",
|
||||
"n8n (https://github.com/n8n-io)"
|
||||
],
|
||||
"license": "ISC",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/n8n-io/n8n"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/node": "^20.9.0",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
1
packages/@n8n/json-schema-to-zod/postcjs.js
Normal file
1
packages/@n8n/json-schema-to-zod/postcjs.js
Normal file
|
@ -0,0 +1 @@
|
|||
require('fs').writeFileSync('./dist/cjs/package.json', '{"type":"commonjs"}', 'utf-8');
|
1
packages/@n8n/json-schema-to-zod/postesm.js
Normal file
1
packages/@n8n/json-schema-to-zod/postesm.js
Normal file
|
@ -0,0 +1 @@
|
|||
require('fs').writeFileSync('./dist/esm/package.json', '{"type":"module"}', 'utf-8');
|
2
packages/@n8n/json-schema-to-zod/src/index.ts
Normal file
2
packages/@n8n/json-schema-to-zod/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export type * from './types';
|
||||
export { jsonSchemaToZod } from './json-schema-to-zod.js';
|
15
packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts
Normal file
15
packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parsers/parse-schema';
|
||||
import type { JsonSchemaToZodOptions, JsonSchema } from './types';
|
||||
|
||||
export const jsonSchemaToZod = <T extends z.ZodTypeAny = z.ZodTypeAny>(
|
||||
schema: JsonSchema,
|
||||
options: JsonSchemaToZodOptions = {},
|
||||
): T => {
|
||||
return parseSchema(schema, {
|
||||
path: [],
|
||||
seen: new Map(),
|
||||
...options,
|
||||
}) as T;
|
||||
};
|
46
packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts
Normal file
46
packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
import { half } from '../utils/half';
|
||||
|
||||
const originalIndex = Symbol('Original index');
|
||||
|
||||
const ensureOriginalIndex = (arr: JsonSchema[]) => {
|
||||
const newArr = [];
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const item = arr[i];
|
||||
if (typeof item === 'boolean') {
|
||||
newArr.push(item ? { [originalIndex]: i } : { [originalIndex]: i, not: {} });
|
||||
} else if (originalIndex in item) {
|
||||
return arr;
|
||||
} else {
|
||||
newArr.push({ ...item, [originalIndex]: i });
|
||||
}
|
||||
}
|
||||
|
||||
return newArr;
|
||||
};
|
||||
|
||||
export function parseAllOf(
|
||||
jsonSchema: JsonSchemaObject & { allOf: JsonSchema[] },
|
||||
refs: Refs,
|
||||
): z.ZodTypeAny {
|
||||
if (jsonSchema.allOf.length === 0) {
|
||||
return z.never();
|
||||
}
|
||||
|
||||
if (jsonSchema.allOf.length === 1) {
|
||||
const item = jsonSchema.allOf[0];
|
||||
|
||||
return parseSchema(item, {
|
||||
...refs,
|
||||
path: [...refs.path, 'allOf', (item as never)[originalIndex]],
|
||||
});
|
||||
}
|
||||
|
||||
const [left, right] = half(ensureOriginalIndex(jsonSchema.allOf));
|
||||
|
||||
return z.intersection(parseAllOf({ allOf: left }, refs), parseAllOf({ allOf: right }, refs));
|
||||
}
|
19
packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts
Normal file
19
packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseAnyOf = (jsonSchema: JsonSchemaObject & { anyOf: JsonSchema[] }, refs: Refs) => {
|
||||
return jsonSchema.anyOf.length
|
||||
? jsonSchema.anyOf.length === 1
|
||||
? parseSchema(jsonSchema.anyOf[0], {
|
||||
...refs,
|
||||
path: [...refs.path, 'anyOf', 0],
|
||||
})
|
||||
: z.union(
|
||||
jsonSchema.anyOf.map((schema, i) =>
|
||||
parseSchema(schema, { ...refs, path: [...refs.path, 'anyOf', i] }),
|
||||
) as [z.ZodTypeAny, z.ZodTypeAny],
|
||||
)
|
||||
: z.any();
|
||||
};
|
34
packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts
Normal file
34
packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, Refs } from '../types';
|
||||
import { extendSchemaWithMessage } from '../utils/extend-schema';
|
||||
|
||||
export const parseArray = (jsonSchema: JsonSchemaObject & { type: 'array' }, refs: Refs) => {
|
||||
if (Array.isArray(jsonSchema.items)) {
|
||||
return z.tuple(
|
||||
jsonSchema.items.map((v, i) =>
|
||||
parseSchema(v, { ...refs, path: [...refs.path, 'items', i] }),
|
||||
) as [z.ZodTypeAny],
|
||||
);
|
||||
}
|
||||
|
||||
let zodSchema = !jsonSchema.items
|
||||
? z.array(z.any())
|
||||
: z.array(parseSchema(jsonSchema.items, { ...refs, path: [...refs.path, 'items'] }));
|
||||
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'minItems',
|
||||
(zs, minItems, errorMessage) => zs.min(minItems, errorMessage),
|
||||
);
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'maxItems',
|
||||
(zs, maxItems, errorMessage) => zs.max(maxItems, errorMessage),
|
||||
);
|
||||
|
||||
return zodSchema;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export const parseBoolean = (_jsonSchema: JsonSchemaObject & { type: 'boolean' }) => {
|
||||
return z.boolean();
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject, Serializable } from '../types';
|
||||
|
||||
export const parseConst = (jsonSchema: JsonSchemaObject & { const: Serializable }) => {
|
||||
return z.literal(jsonSchema.const as z.Primitive);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export const parseDefault = (_jsonSchema: JsonSchemaObject) => {
|
||||
return z.any();
|
||||
};
|
25
packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts
Normal file
25
packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject, Serializable } from '../types';
|
||||
|
||||
export const parseEnum = (jsonSchema: JsonSchemaObject & { enum: Serializable[] }) => {
|
||||
if (jsonSchema.enum.length === 0) {
|
||||
return z.never();
|
||||
}
|
||||
|
||||
if (jsonSchema.enum.length === 1) {
|
||||
// union does not work when there is only one element
|
||||
return z.literal(jsonSchema.enum[0] as z.Primitive);
|
||||
}
|
||||
|
||||
if (jsonSchema.enum.every((x) => typeof x === 'string')) {
|
||||
return z.enum(jsonSchema.enum as [string]);
|
||||
}
|
||||
|
||||
return z.union(
|
||||
jsonSchema.enum.map((x) => z.literal(x as z.Primitive)) as unknown as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
],
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseIfThenElse = (
|
||||
jsonSchema: JsonSchemaObject & {
|
||||
if: JsonSchema;
|
||||
then: JsonSchema;
|
||||
else: JsonSchema;
|
||||
},
|
||||
refs: Refs,
|
||||
) => {
|
||||
const $if = parseSchema(jsonSchema.if, { ...refs, path: [...refs.path, 'if'] });
|
||||
const $then = parseSchema(jsonSchema.then, {
|
||||
...refs,
|
||||
path: [...refs.path, 'then'],
|
||||
});
|
||||
const $else = parseSchema(jsonSchema.else, {
|
||||
...refs,
|
||||
path: [...refs.path, 'else'],
|
||||
});
|
||||
|
||||
return z.union([$then, $else]).superRefine((value, ctx) => {
|
||||
const result = $if.safeParse(value).success ? $then.safeParse(value) : $else.safeParse(value);
|
||||
|
||||
if (!result.success) {
|
||||
result.error.errors.forEach((error) => ctx.addIssue(error));
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchema, JsonSchemaObject, Refs } from '../types';
|
||||
|
||||
export const parseMultipleType = (
|
||||
jsonSchema: JsonSchemaObject & { type: string[] },
|
||||
refs: Refs,
|
||||
) => {
|
||||
return z.union(
|
||||
jsonSchema.type.map((type) => parseSchema({ ...jsonSchema, type } as JsonSchema, refs)) as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
],
|
||||
);
|
||||
};
|
15
packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts
Normal file
15
packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseNot = (jsonSchema: JsonSchemaObject & { not: JsonSchema }, refs: Refs) => {
|
||||
return z.any().refine(
|
||||
(value) =>
|
||||
!parseSchema(jsonSchema.not, {
|
||||
...refs,
|
||||
path: [...refs.path, 'not'],
|
||||
}).safeParse(value).success,
|
||||
'Invalid input: Should NOT be valid against schema',
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export const parseNull = (_jsonSchema: JsonSchemaObject & { type: 'null' }) => {
|
||||
return z.null();
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, Refs } from '../types';
|
||||
import { omit } from '../utils/omit';
|
||||
|
||||
/**
|
||||
* For compatibility with open api 3.0 nullable
|
||||
*/
|
||||
export const parseNullable = (jsonSchema: JsonSchemaObject & { nullable: true }, refs: Refs) => {
|
||||
return parseSchema(omit(jsonSchema, 'nullable'), refs, true).nullable();
|
||||
};
|
88
packages/@n8n/json-schema-to-zod/src/parsers/parse-number.ts
Normal file
88
packages/@n8n/json-schema-to-zod/src/parsers/parse-number.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
import { extendSchemaWithMessage } from '../utils/extend-schema';
|
||||
|
||||
export const parseNumber = (jsonSchema: JsonSchemaObject & { type: 'number' | 'integer' }) => {
|
||||
let zodSchema = z.number();
|
||||
|
||||
let isInteger = false;
|
||||
if (jsonSchema.type === 'integer') {
|
||||
isInteger = true;
|
||||
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'type', (zs, _, errorMsg) =>
|
||||
zs.int(errorMsg),
|
||||
);
|
||||
} else if (jsonSchema.format === 'int64') {
|
||||
isInteger = true;
|
||||
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'format', (zs, _, errorMsg) =>
|
||||
zs.int(errorMsg),
|
||||
);
|
||||
}
|
||||
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'multipleOf',
|
||||
(zs, multipleOf, errorMsg) => {
|
||||
if (multipleOf === 1) {
|
||||
if (isInteger) return zs;
|
||||
|
||||
return zs.int(errorMsg);
|
||||
}
|
||||
|
||||
return zs.multipleOf(multipleOf, errorMsg);
|
||||
},
|
||||
);
|
||||
|
||||
if (typeof jsonSchema.minimum === 'number') {
|
||||
if (jsonSchema.exclusiveMinimum === true) {
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'minimum',
|
||||
(zs, minimum, errorMsg) => zs.gt(minimum, errorMsg),
|
||||
);
|
||||
} else {
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'minimum',
|
||||
(zs, minimum, errorMsg) => zs.gte(minimum, errorMsg),
|
||||
);
|
||||
}
|
||||
} else if (typeof jsonSchema.exclusiveMinimum === 'number') {
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'exclusiveMinimum',
|
||||
(zs, exclusiveMinimum, errorMsg) => zs.gt(exclusiveMinimum as number, errorMsg),
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof jsonSchema.maximum === 'number') {
|
||||
if (jsonSchema.exclusiveMaximum === true) {
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'maximum',
|
||||
(zs, maximum, errorMsg) => zs.lt(maximum, errorMsg),
|
||||
);
|
||||
} else {
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'maximum',
|
||||
(zs, maximum, errorMsg) => zs.lte(maximum, errorMsg),
|
||||
);
|
||||
}
|
||||
} else if (typeof jsonSchema.exclusiveMaximum === 'number') {
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'exclusiveMaximum',
|
||||
(zs, exclusiveMaximum, errorMsg) => zs.lt(exclusiveMaximum as number, errorMsg),
|
||||
);
|
||||
}
|
||||
|
||||
return zodSchema;
|
||||
};
|
219
packages/@n8n/json-schema-to-zod/src/parsers/parse-object.ts
Normal file
219
packages/@n8n/json-schema-to-zod/src/parsers/parse-object.ts
Normal file
|
@ -0,0 +1,219 @@
|
|||
import * as z from 'zod';
|
||||
|
||||
import { parseAllOf } from './parse-all-of';
|
||||
import { parseAnyOf } from './parse-any-of';
|
||||
import { parseOneOf } from './parse-one-of';
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, Refs } from '../types';
|
||||
import { its } from '../utils/its';
|
||||
|
||||
function parseObjectProperties(objectSchema: JsonSchemaObject & { type: 'object' }, refs: Refs) {
|
||||
if (!objectSchema.properties) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const propertyKeys = Object.keys(objectSchema.properties);
|
||||
if (propertyKeys.length === 0) {
|
||||
return z.object({});
|
||||
}
|
||||
|
||||
const properties: Record<string, z.ZodTypeAny> = {};
|
||||
|
||||
for (const key of propertyKeys) {
|
||||
const propJsonSchema = objectSchema.properties[key];
|
||||
|
||||
const propZodSchema = parseSchema(propJsonSchema, {
|
||||
...refs,
|
||||
path: [...refs.path, 'properties', key],
|
||||
});
|
||||
|
||||
const hasDefault = typeof propJsonSchema === 'object' && propJsonSchema.default !== undefined;
|
||||
|
||||
const required = Array.isArray(objectSchema.required)
|
||||
? objectSchema.required.includes(key)
|
||||
: typeof propJsonSchema === 'object' && propJsonSchema.required === true;
|
||||
|
||||
const isOptional = !hasDefault && !required;
|
||||
|
||||
properties[key] = isOptional ? propZodSchema.optional() : propZodSchema;
|
||||
}
|
||||
|
||||
return z.object(properties);
|
||||
}
|
||||
|
||||
export function parseObject(
|
||||
objectSchema: JsonSchemaObject & { type: 'object' },
|
||||
refs: Refs,
|
||||
): z.ZodTypeAny {
|
||||
const hasPatternProperties = Object.keys(objectSchema.patternProperties ?? {}).length > 0;
|
||||
|
||||
const propertiesSchema:
|
||||
| z.ZodObject<Record<string, z.ZodTypeAny>, 'strip', z.ZodTypeAny>
|
||||
| undefined = parseObjectProperties(objectSchema, refs);
|
||||
let zodSchema: z.ZodTypeAny | undefined = propertiesSchema;
|
||||
|
||||
const additionalProperties =
|
||||
objectSchema.additionalProperties !== undefined
|
||||
? parseSchema(objectSchema.additionalProperties, {
|
||||
...refs,
|
||||
path: [...refs.path, 'additionalProperties'],
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (objectSchema.patternProperties) {
|
||||
const parsedPatternProperties = Object.fromEntries(
|
||||
Object.entries(objectSchema.patternProperties).map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
parseSchema(value, {
|
||||
...refs,
|
||||
path: [...refs.path, 'patternProperties', key],
|
||||
}),
|
||||
];
|
||||
}),
|
||||
);
|
||||
const patternPropertyValues = Object.values(parsedPatternProperties);
|
||||
|
||||
if (propertiesSchema) {
|
||||
if (additionalProperties) {
|
||||
zodSchema = propertiesSchema.catchall(
|
||||
z.union([...patternPropertyValues, additionalProperties] as [z.ZodTypeAny, z.ZodTypeAny]),
|
||||
);
|
||||
} else if (Object.keys(parsedPatternProperties).length > 1) {
|
||||
zodSchema = propertiesSchema.catchall(
|
||||
z.union(patternPropertyValues as [z.ZodTypeAny, z.ZodTypeAny]),
|
||||
);
|
||||
} else {
|
||||
zodSchema = propertiesSchema.catchall(patternPropertyValues[0]);
|
||||
}
|
||||
} else {
|
||||
if (additionalProperties) {
|
||||
zodSchema = z.record(
|
||||
z.union([...patternPropertyValues, additionalProperties] as [z.ZodTypeAny, z.ZodTypeAny]),
|
||||
);
|
||||
} else if (patternPropertyValues.length > 1) {
|
||||
zodSchema = z.record(z.union(patternPropertyValues as [z.ZodTypeAny, z.ZodTypeAny]));
|
||||
} else {
|
||||
zodSchema = z.record(patternPropertyValues[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const objectPropertyKeys = new Set(Object.keys(objectSchema.properties ?? {}));
|
||||
zodSchema = zodSchema.superRefine((value: Record<string, unknown>, ctx) => {
|
||||
for (const key in value) {
|
||||
let wasMatched = objectPropertyKeys.has(key);
|
||||
|
||||
for (const patternPropertyKey in objectSchema.patternProperties) {
|
||||
const regex = new RegExp(patternPropertyKey);
|
||||
if (key.match(regex)) {
|
||||
wasMatched = true;
|
||||
const result = parsedPatternProperties[patternPropertyKey].safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasMatched && additionalProperties) {
|
||||
const result = additionalProperties.safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: 'Invalid input: must match catchall schema',
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let output: z.ZodTypeAny;
|
||||
if (propertiesSchema) {
|
||||
if (hasPatternProperties) {
|
||||
output = zodSchema!;
|
||||
} else if (additionalProperties) {
|
||||
if (additionalProperties instanceof z.ZodNever) {
|
||||
output = propertiesSchema.strict();
|
||||
} else {
|
||||
output = propertiesSchema.catchall(additionalProperties);
|
||||
}
|
||||
} else {
|
||||
output = zodSchema!;
|
||||
}
|
||||
} else {
|
||||
if (hasPatternProperties) {
|
||||
output = zodSchema!;
|
||||
} else if (additionalProperties) {
|
||||
output = z.record(additionalProperties);
|
||||
} else {
|
||||
output = z.record(z.any());
|
||||
}
|
||||
}
|
||||
|
||||
if (its.an.anyOf(objectSchema)) {
|
||||
output = output.and(
|
||||
parseAnyOf(
|
||||
{
|
||||
...objectSchema,
|
||||
anyOf: objectSchema.anyOf.map((x) =>
|
||||
typeof x === 'object' &&
|
||||
!x.type &&
|
||||
(x.properties ?? x.additionalProperties ?? x.patternProperties)
|
||||
? { ...x, type: 'object' }
|
||||
: x,
|
||||
),
|
||||
},
|
||||
refs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (its.a.oneOf(objectSchema)) {
|
||||
output = output.and(
|
||||
parseOneOf(
|
||||
{
|
||||
...objectSchema,
|
||||
oneOf: objectSchema.oneOf.map((x) =>
|
||||
typeof x === 'object' &&
|
||||
!x.type &&
|
||||
(x.properties ?? x.additionalProperties ?? x.patternProperties)
|
||||
? { ...x, type: 'object' }
|
||||
: x,
|
||||
),
|
||||
},
|
||||
refs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (its.an.allOf(objectSchema)) {
|
||||
output = output.and(
|
||||
parseAllOf(
|
||||
{
|
||||
...objectSchema,
|
||||
allOf: objectSchema.allOf.map((x) =>
|
||||
typeof x === 'object' &&
|
||||
!x.type &&
|
||||
(x.properties ?? x.additionalProperties ?? x.patternProperties)
|
||||
? { ...x, type: 'object' }
|
||||
: x,
|
||||
),
|
||||
},
|
||||
refs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
41
packages/@n8n/json-schema-to-zod/src/parsers/parse-one-of.ts
Normal file
41
packages/@n8n/json-schema-to-zod/src/parsers/parse-one-of.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseOneOf = (jsonSchema: JsonSchemaObject & { oneOf: JsonSchema[] }, refs: Refs) => {
|
||||
if (!jsonSchema.oneOf.length) {
|
||||
return z.any();
|
||||
}
|
||||
|
||||
if (jsonSchema.oneOf.length === 1) {
|
||||
return parseSchema(jsonSchema.oneOf[0], {
|
||||
...refs,
|
||||
path: [...refs.path, 'oneOf', 0],
|
||||
});
|
||||
}
|
||||
|
||||
return z.any().superRefine((x, ctx) => {
|
||||
const schemas = jsonSchema.oneOf.map((schema, i) =>
|
||||
parseSchema(schema, {
|
||||
...refs,
|
||||
path: [...refs.path, 'oneOf', i],
|
||||
}),
|
||||
);
|
||||
|
||||
const unionErrors = schemas.reduce<z.ZodError[]>(
|
||||
(errors, schema) =>
|
||||
((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)),
|
||||
[],
|
||||
);
|
||||
|
||||
if (schemas.length - unionErrors.length !== 1) {
|
||||
ctx.addIssue({
|
||||
path: ctx.path,
|
||||
code: 'invalid_union',
|
||||
unionErrors,
|
||||
message: 'Invalid input: Should pass single schema',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
130
packages/@n8n/json-schema-to-zod/src/parsers/parse-schema.ts
Normal file
130
packages/@n8n/json-schema-to-zod/src/parsers/parse-schema.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
import * as z from 'zod';
|
||||
|
||||
import { parseAllOf } from './parse-all-of';
|
||||
import { parseAnyOf } from './parse-any-of';
|
||||
import { parseArray } from './parse-array';
|
||||
import { parseBoolean } from './parse-boolean';
|
||||
import { parseConst } from './parse-const';
|
||||
import { parseDefault } from './parse-default';
|
||||
import { parseEnum } from './parse-enum';
|
||||
import { parseIfThenElse } from './parse-if-then-else';
|
||||
import { parseMultipleType } from './parse-multiple-type';
|
||||
import { parseNot } from './parse-not';
|
||||
import { parseNull } from './parse-null';
|
||||
import { parseNullable } from './parse-nullable';
|
||||
import { parseNumber } from './parse-number';
|
||||
import { parseObject } from './parse-object';
|
||||
import { parseOneOf } from './parse-one-of';
|
||||
import { parseString } from './parse-string';
|
||||
import type { ParserSelector, Refs, JsonSchemaObject, JsonSchema } from '../types';
|
||||
import { its } from '../utils/its';
|
||||
|
||||
const addDescribes = (jsonSchema: JsonSchemaObject, zodSchema: z.ZodTypeAny): z.ZodTypeAny => {
|
||||
if (jsonSchema.description) {
|
||||
zodSchema = zodSchema.describe(jsonSchema.description);
|
||||
}
|
||||
|
||||
return zodSchema;
|
||||
};
|
||||
|
||||
const addDefaults = (jsonSchema: JsonSchemaObject, zodSchema: z.ZodTypeAny): z.ZodTypeAny => {
|
||||
if (jsonSchema.default !== undefined) {
|
||||
zodSchema = zodSchema.default(jsonSchema.default);
|
||||
}
|
||||
|
||||
return zodSchema;
|
||||
};
|
||||
|
||||
const addAnnotations = (jsonSchema: JsonSchemaObject, zodSchema: z.ZodTypeAny): z.ZodTypeAny => {
|
||||
if (jsonSchema.readOnly) {
|
||||
zodSchema = zodSchema.readonly();
|
||||
}
|
||||
|
||||
return zodSchema;
|
||||
};
|
||||
|
||||
const selectParser: ParserSelector = (schema, refs) => {
|
||||
if (its.a.nullable(schema)) {
|
||||
return parseNullable(schema, refs);
|
||||
} else if (its.an.object(schema)) {
|
||||
return parseObject(schema, refs);
|
||||
} else if (its.an.array(schema)) {
|
||||
return parseArray(schema, refs);
|
||||
} else if (its.an.anyOf(schema)) {
|
||||
return parseAnyOf(schema, refs);
|
||||
} else if (its.an.allOf(schema)) {
|
||||
return parseAllOf(schema, refs);
|
||||
} else if (its.a.oneOf(schema)) {
|
||||
return parseOneOf(schema, refs);
|
||||
} else if (its.a.not(schema)) {
|
||||
return parseNot(schema, refs);
|
||||
} else if (its.an.enum(schema)) {
|
||||
return parseEnum(schema); //<-- needs to come before primitives
|
||||
} else if (its.a.const(schema)) {
|
||||
return parseConst(schema);
|
||||
} else if (its.a.multipleType(schema)) {
|
||||
return parseMultipleType(schema, refs);
|
||||
} else if (its.a.primitive(schema, 'string')) {
|
||||
return parseString(schema);
|
||||
} else if (its.a.primitive(schema, 'number') || its.a.primitive(schema, 'integer')) {
|
||||
return parseNumber(schema);
|
||||
} else if (its.a.primitive(schema, 'boolean')) {
|
||||
return parseBoolean(schema);
|
||||
} else if (its.a.primitive(schema, 'null')) {
|
||||
return parseNull(schema);
|
||||
} else if (its.a.conditional(schema)) {
|
||||
return parseIfThenElse(schema, refs);
|
||||
} else {
|
||||
return parseDefault(schema);
|
||||
}
|
||||
};
|
||||
|
||||
export const parseSchema = (
|
||||
jsonSchema: JsonSchema,
|
||||
refs: Refs = { seen: new Map(), path: [] },
|
||||
blockMeta?: boolean,
|
||||
): z.ZodTypeAny => {
|
||||
if (typeof jsonSchema !== 'object') return jsonSchema ? z.any() : z.never();
|
||||
|
||||
if (refs.parserOverride) {
|
||||
const custom = refs.parserOverride(jsonSchema, refs);
|
||||
|
||||
if (custom instanceof z.ZodType) {
|
||||
return custom;
|
||||
}
|
||||
}
|
||||
|
||||
let seen = refs.seen.get(jsonSchema);
|
||||
|
||||
if (seen) {
|
||||
if (seen.r !== undefined) {
|
||||
return seen.r;
|
||||
}
|
||||
|
||||
if (refs.depth === undefined || seen.n >= refs.depth) {
|
||||
return z.any();
|
||||
}
|
||||
|
||||
seen.n += 1;
|
||||
} else {
|
||||
seen = { r: undefined, n: 0 };
|
||||
refs.seen.set(jsonSchema, seen);
|
||||
}
|
||||
|
||||
let parsedZodSchema = selectParser(jsonSchema, refs);
|
||||
if (!blockMeta) {
|
||||
if (!refs.withoutDescribes) {
|
||||
parsedZodSchema = addDescribes(jsonSchema, parsedZodSchema);
|
||||
}
|
||||
|
||||
if (!refs.withoutDefaults) {
|
||||
parsedZodSchema = addDefaults(jsonSchema, parsedZodSchema);
|
||||
}
|
||||
|
||||
parsedZodSchema = addAnnotations(jsonSchema, parsedZodSchema);
|
||||
}
|
||||
|
||||
seen.r = parsedZodSchema;
|
||||
|
||||
return parsedZodSchema;
|
||||
};
|
58
packages/@n8n/json-schema-to-zod/src/parsers/parse-string.ts
Normal file
58
packages/@n8n/json-schema-to-zod/src/parsers/parse-string.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
import { extendSchemaWithMessage } from '../utils/extend-schema';
|
||||
|
||||
export const parseString = (jsonSchema: JsonSchemaObject & { type: 'string' }) => {
|
||||
let zodSchema = z.string();
|
||||
|
||||
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'format', (zs, format, errorMsg) => {
|
||||
switch (format) {
|
||||
case 'email':
|
||||
return zs.email(errorMsg);
|
||||
case 'ip':
|
||||
return zs.ip(errorMsg);
|
||||
case 'ipv4':
|
||||
return zs.ip({ version: 'v4', message: errorMsg });
|
||||
case 'ipv6':
|
||||
return zs.ip({ version: 'v6', message: errorMsg });
|
||||
case 'uri':
|
||||
return zs.url(errorMsg);
|
||||
case 'uuid':
|
||||
return zs.uuid(errorMsg);
|
||||
case 'date-time':
|
||||
return zs.datetime({ offset: true, message: errorMsg });
|
||||
case 'time':
|
||||
return zs.time(errorMsg);
|
||||
case 'date':
|
||||
return zs.date(errorMsg);
|
||||
case 'binary':
|
||||
return zs.base64(errorMsg);
|
||||
case 'duration':
|
||||
return zs.duration(errorMsg);
|
||||
default:
|
||||
return zs;
|
||||
}
|
||||
});
|
||||
|
||||
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'contentEncoding', (zs, _, errorMsg) =>
|
||||
zs.base64(errorMsg),
|
||||
);
|
||||
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'pattern', (zs, pattern, errorMsg) =>
|
||||
zs.regex(new RegExp(pattern), errorMsg),
|
||||
);
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'minLength',
|
||||
(zs, minLength, errorMsg) => zs.min(minLength, errorMsg),
|
||||
);
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'maxLength',
|
||||
(zs, maxLength, errorMsg) => zs.max(maxLength, errorMsg),
|
||||
);
|
||||
|
||||
return zodSchema;
|
||||
};
|
82
packages/@n8n/json-schema-to-zod/src/types.ts
Normal file
82
packages/@n8n/json-schema-to-zod/src/types.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import type { ZodTypeAny } from 'zod';
|
||||
|
||||
export type Serializable =
|
||||
| { [key: string]: Serializable }
|
||||
| Serializable[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
|
||||
export type JsonSchema = JsonSchemaObject | boolean;
|
||||
export type JsonSchemaObject = {
|
||||
// left permissive by design
|
||||
type?: string | string[];
|
||||
|
||||
// object
|
||||
properties?: { [key: string]: JsonSchema };
|
||||
additionalProperties?: JsonSchema;
|
||||
unevaluatedProperties?: JsonSchema;
|
||||
patternProperties?: { [key: string]: JsonSchema };
|
||||
minProperties?: number;
|
||||
maxProperties?: number;
|
||||
required?: string[] | boolean;
|
||||
propertyNames?: JsonSchema;
|
||||
|
||||
// array
|
||||
items?: JsonSchema | JsonSchema[];
|
||||
additionalItems?: JsonSchema;
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
uniqueItems?: boolean;
|
||||
|
||||
// string
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
pattern?: string;
|
||||
format?: string;
|
||||
|
||||
// number
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
exclusiveMinimum?: number | boolean;
|
||||
exclusiveMaximum?: number | boolean;
|
||||
multipleOf?: number;
|
||||
|
||||
// unions
|
||||
anyOf?: JsonSchema[];
|
||||
allOf?: JsonSchema[];
|
||||
oneOf?: JsonSchema[];
|
||||
|
||||
if?: JsonSchema;
|
||||
then?: JsonSchema;
|
||||
else?: JsonSchema;
|
||||
|
||||
// shared
|
||||
const?: Serializable;
|
||||
enum?: Serializable[];
|
||||
|
||||
errorMessage?: { [key: string]: string | undefined };
|
||||
|
||||
description?: string;
|
||||
default?: Serializable;
|
||||
readOnly?: boolean;
|
||||
not?: JsonSchema;
|
||||
contentEncoding?: string;
|
||||
nullable?: boolean;
|
||||
};
|
||||
|
||||
export type ParserSelector = (schema: JsonSchemaObject, refs: Refs) => ZodTypeAny;
|
||||
export type ParserOverride = (schema: JsonSchemaObject, refs: Refs) => ZodTypeAny | undefined;
|
||||
|
||||
export type JsonSchemaToZodOptions = {
|
||||
withoutDefaults?: boolean;
|
||||
withoutDescribes?: boolean;
|
||||
parserOverride?: ParserOverride;
|
||||
depth?: number;
|
||||
};
|
||||
|
||||
export type Refs = JsonSchemaToZodOptions & {
|
||||
path: Array<string | number>;
|
||||
seen: Map<object | boolean, { n: number; r: ZodTypeAny | undefined }>;
|
||||
};
|
23
packages/@n8n/json-schema-to-zod/src/utils/extend-schema.ts
Normal file
23
packages/@n8n/json-schema-to-zod/src/utils/extend-schema.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import type { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export function extendSchemaWithMessage<
|
||||
TZod extends z.ZodTypeAny,
|
||||
TJson extends JsonSchemaObject,
|
||||
TKey extends keyof TJson,
|
||||
>(
|
||||
zodSchema: TZod,
|
||||
jsonSchema: TJson,
|
||||
key: TKey,
|
||||
extend: (zodSchema: TZod, value: NonNullable<TJson[TKey]>, errorMessage?: string) => TZod,
|
||||
) {
|
||||
const value = jsonSchema[key];
|
||||
|
||||
if (value !== undefined) {
|
||||
const errorMessage = jsonSchema.errorMessage?.[key as string];
|
||||
return extend(zodSchema, value as NonNullable<TJson[TKey]>, errorMessage);
|
||||
}
|
||||
|
||||
return zodSchema;
|
||||
}
|
3
packages/@n8n/json-schema-to-zod/src/utils/half.ts
Normal file
3
packages/@n8n/json-schema-to-zod/src/utils/half.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const half = <T>(arr: T[]): [T[], T[]] => {
|
||||
return [arr.slice(0, arr.length / 2), arr.slice(arr.length / 2)];
|
||||
};
|
57
packages/@n8n/json-schema-to-zod/src/utils/its.ts
Normal file
57
packages/@n8n/json-schema-to-zod/src/utils/its.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import type { JsonSchema, JsonSchemaObject, Serializable } from '../types';
|
||||
|
||||
export const its = {
|
||||
an: {
|
||||
object: (x: JsonSchemaObject): x is JsonSchemaObject & { type: 'object' } =>
|
||||
x.type === 'object',
|
||||
array: (x: JsonSchemaObject): x is JsonSchemaObject & { type: 'array' } => x.type === 'array',
|
||||
anyOf: (
|
||||
x: JsonSchemaObject,
|
||||
): x is JsonSchemaObject & {
|
||||
anyOf: JsonSchema[];
|
||||
} => x.anyOf !== undefined,
|
||||
allOf: (
|
||||
x: JsonSchemaObject,
|
||||
): x is JsonSchemaObject & {
|
||||
allOf: JsonSchema[];
|
||||
} => x.allOf !== undefined,
|
||||
enum: (
|
||||
x: JsonSchemaObject,
|
||||
): x is JsonSchemaObject & {
|
||||
enum: Serializable | Serializable[];
|
||||
} => x.enum !== undefined,
|
||||
},
|
||||
a: {
|
||||
nullable: (x: JsonSchemaObject): x is JsonSchemaObject & { nullable: true } =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
(x as any).nullable === true,
|
||||
multipleType: (x: JsonSchemaObject): x is JsonSchemaObject & { type: string[] } =>
|
||||
Array.isArray(x.type),
|
||||
not: (
|
||||
x: JsonSchemaObject,
|
||||
): x is JsonSchemaObject & {
|
||||
not: JsonSchema;
|
||||
} => x.not !== undefined,
|
||||
const: (
|
||||
x: JsonSchemaObject,
|
||||
): x is JsonSchemaObject & {
|
||||
const: Serializable;
|
||||
} => x.const !== undefined,
|
||||
primitive: <T extends 'string' | 'number' | 'integer' | 'boolean' | 'null'>(
|
||||
x: JsonSchemaObject,
|
||||
p: T,
|
||||
): x is JsonSchemaObject & { type: T } => x.type === p,
|
||||
conditional: (
|
||||
x: JsonSchemaObject,
|
||||
): x is JsonSchemaObject & {
|
||||
if: JsonSchema;
|
||||
then: JsonSchema;
|
||||
else: JsonSchema;
|
||||
} => Boolean('if' in x && x.if && 'then' in x && 'else' in x && x.then && x.else),
|
||||
oneOf: (
|
||||
x: JsonSchemaObject,
|
||||
): x is JsonSchemaObject & {
|
||||
oneOf: JsonSchema[];
|
||||
} => x.oneOf !== undefined,
|
||||
},
|
||||
};
|
8
packages/@n8n/json-schema-to-zod/src/utils/omit.ts
Normal file
8
packages/@n8n/json-schema-to-zod/src/utils/omit.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export const omit = <T extends object, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> =>
|
||||
Object.keys(obj).reduce((acc: Record<string, unknown>, key) => {
|
||||
if (!keys.includes(key as K)) {
|
||||
acc[key] = obj[key as K];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}) as Omit<T, K>;
|
143
packages/@n8n/json-schema-to-zod/test/all.json
Normal file
143
packages/@n8n/json-schema-to-zod/test/all.json
Normal file
|
@ -0,0 +1,143 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"properties": {
|
||||
"allOf": {
|
||||
"allOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"anyOf": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oneOf": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"array": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 2,
|
||||
"maxItems": 3
|
||||
},
|
||||
"tuple": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"minItems": 2,
|
||||
"maxItems": 3
|
||||
},
|
||||
"const": {
|
||||
"const": "xbox"
|
||||
},
|
||||
"enum": {
|
||||
"enum": ["ps4", "ps5"]
|
||||
},
|
||||
"ifThenElse": {
|
||||
"if": {
|
||||
"type": "string"
|
||||
},
|
||||
"then": {
|
||||
"const": "x"
|
||||
},
|
||||
"else": {
|
||||
"enum": [1, 2, 3]
|
||||
}
|
||||
},
|
||||
"null": {
|
||||
"type": "null"
|
||||
},
|
||||
"multiple": {
|
||||
"type": ["array", "boolean"]
|
||||
},
|
||||
"objAdditionalTrue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"objAdditionalFalse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"objAdditionalNumber": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"objAdditionalOnly": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"patternProps": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^x": {
|
||||
"type": "string"
|
||||
},
|
||||
"^y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"z": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
16
packages/@n8n/json-schema-to-zod/test/extend-expect.ts
Normal file
16
packages/@n8n/json-schema-to-zod/test/extend-expect.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import type { z } from 'zod';
|
||||
|
||||
expect.extend({
|
||||
toMatchZod(this: jest.MatcherContext, actual: z.ZodTypeAny, expected: z.ZodTypeAny) {
|
||||
const actualSerialized = JSON.stringify(actual._def, null, 2);
|
||||
const expectedSerialized = JSON.stringify(expected._def, null, 2);
|
||||
const pass = this.equals(actualSerialized, expectedSerialized);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: pass
|
||||
? () => `Expected ${actualSerialized} not to match ${expectedSerialized}`
|
||||
: () => `Expected ${actualSerialized} to match ${expectedSerialized}`,
|
||||
};
|
||||
},
|
||||
});
|
5
packages/@n8n/json-schema-to-zod/test/jest.d.ts
vendored
Normal file
5
packages/@n8n/json-schema-to-zod/test/jest.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
namespace jest {
|
||||
interface Matchers<R, T> {
|
||||
toMatchZod(expected: unknown): T;
|
||||
}
|
||||
}
|
106
packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts
Normal file
106
packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import type { JSONSchema4, JSONSchema6Definition, JSONSchema7Definition } from 'json-schema';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { jsonSchemaToZod } from '../src';
|
||||
|
||||
describe('jsonSchemaToZod', () => {
|
||||
test('should accept json schema 7 and 4', () => {
|
||||
const schema = { type: 'string' } as unknown;
|
||||
|
||||
expect(jsonSchemaToZod(schema as JSONSchema4));
|
||||
expect(jsonSchemaToZod(schema as JSONSchema6Definition));
|
||||
expect(jsonSchemaToZod(schema as JSONSchema7Definition));
|
||||
});
|
||||
|
||||
test('can exclude defaults', () => {
|
||||
expect(
|
||||
jsonSchemaToZod(
|
||||
{
|
||||
type: 'string',
|
||||
default: 'foo',
|
||||
},
|
||||
{ withoutDefaults: true },
|
||||
),
|
||||
).toMatchZod(z.string());
|
||||
});
|
||||
|
||||
test('should include describes', () => {
|
||||
expect(
|
||||
jsonSchemaToZod({
|
||||
type: 'string',
|
||||
description: 'foo',
|
||||
}),
|
||||
).toMatchZod(z.string().describe('foo'));
|
||||
});
|
||||
|
||||
test('can exclude describes', () => {
|
||||
expect(
|
||||
jsonSchemaToZod(
|
||||
{
|
||||
type: 'string',
|
||||
description: 'foo',
|
||||
},
|
||||
{
|
||||
withoutDescribes: true,
|
||||
},
|
||||
),
|
||||
).toMatchZod(z.string());
|
||||
});
|
||||
|
||||
test('will remove optionality if default is present', () => {
|
||||
expect(
|
||||
jsonSchemaToZod({
|
||||
type: 'object',
|
||||
properties: {
|
||||
prop: {
|
||||
type: 'string',
|
||||
default: 'def',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toMatchZod(z.object({ prop: z.string().default('def') }));
|
||||
});
|
||||
|
||||
test('will handle falsy defaults', () => {
|
||||
expect(
|
||||
jsonSchemaToZod({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}),
|
||||
).toMatchZod(z.boolean().default(false));
|
||||
});
|
||||
|
||||
test('will ignore undefined as default', () => {
|
||||
expect(
|
||||
jsonSchemaToZod({
|
||||
type: 'null',
|
||||
default: undefined,
|
||||
}),
|
||||
).toMatchZod(z.null());
|
||||
});
|
||||
|
||||
test('should be possible to define a custom parser', () => {
|
||||
expect(
|
||||
jsonSchemaToZod(
|
||||
{
|
||||
allOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean', description: 'foo' }],
|
||||
},
|
||||
{
|
||||
parserOverride: (schema, refs) => {
|
||||
if (
|
||||
refs.path.length === 2 &&
|
||||
refs.path[0] === 'allOf' &&
|
||||
refs.path[1] === 2 &&
|
||||
schema.type === 'boolean' &&
|
||||
schema.description === 'foo'
|
||||
) {
|
||||
return z.null();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
),
|
||||
).toMatchZod(z.intersection(z.string(), z.intersection(z.number(), z.null())));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseAllOf } from '../../src/parsers/parse-all-of';
|
||||
|
||||
describe('parseAllOf', () => {
|
||||
test('should create never if empty', () => {
|
||||
expect(
|
||||
parseAllOf(
|
||||
{
|
||||
allOf: [],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.never());
|
||||
});
|
||||
|
||||
test('should handle true values', () => {
|
||||
expect(
|
||||
parseAllOf(
|
||||
{
|
||||
allOf: [{ type: 'string' }, true],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.intersection(z.string(), z.any()));
|
||||
});
|
||||
|
||||
test('should handle false values', () => {
|
||||
expect(
|
||||
parseAllOf(
|
||||
{
|
||||
allOf: [{ type: 'string' }, false],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.intersection(
|
||||
z.string(),
|
||||
z
|
||||
.any()
|
||||
.refine(
|
||||
(value) => !z.any().safeParse(value).success,
|
||||
'Invalid input: Should NOT be valid against schema',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseAnyOf } from '../../src/parsers/parse-any-of';
|
||||
|
||||
describe('parseAnyOf', () => {
|
||||
test('should create a union from two or more schemas', () => {
|
||||
expect(
|
||||
parseAnyOf(
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{ type: 'number' },
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.union([z.string(), z.number()]));
|
||||
});
|
||||
|
||||
test('should extract a single schema', () => {
|
||||
expect(parseAnyOf({ anyOf: [{ type: 'string' }] }, { path: [], seen: new Map() })).toMatchZod(
|
||||
z.string(),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return z.any() if array is empty', () => {
|
||||
expect(parseAnyOf({ anyOf: [] }, { path: [], seen: new Map() })).toMatchZod(z.any());
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseArray } from '../../src/parsers/parse-array';
|
||||
|
||||
describe('parseArray', () => {
|
||||
test('should create tuple with items array', () => {
|
||||
expect(
|
||||
parseArray(
|
||||
{
|
||||
type: 'array',
|
||||
items: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.tuple([z.string(), z.number()]));
|
||||
});
|
||||
|
||||
test('should create array with items object', () => {
|
||||
expect(
|
||||
parseArray(
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.array(z.string()));
|
||||
});
|
||||
|
||||
test('should create min for minItems', () => {
|
||||
expect(
|
||||
parseArray(
|
||||
{
|
||||
type: 'array',
|
||||
minItems: 2,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.array(z.string()).min(2));
|
||||
});
|
||||
|
||||
test('should create max for maxItems', () => {
|
||||
expect(
|
||||
parseArray(
|
||||
{
|
||||
type: 'array',
|
||||
maxItems: 2,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.array(z.string()).max(2));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseConst } from '../../src/parsers/parse-const';
|
||||
|
||||
describe('parseConst', () => {
|
||||
test('should handle falsy constants', () => {
|
||||
expect(
|
||||
parseConst({
|
||||
const: false,
|
||||
}),
|
||||
).toMatchZod(z.literal(false));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseEnum } from '../../src/parsers/parse-enum';
|
||||
|
||||
describe('parseEnum', () => {
|
||||
test('should create never with empty enum', () => {
|
||||
expect(
|
||||
parseEnum({
|
||||
enum: [],
|
||||
}),
|
||||
).toMatchZod(z.never());
|
||||
});
|
||||
|
||||
test('should create literal with single item enum', () => {
|
||||
expect(
|
||||
parseEnum({
|
||||
enum: ['someValue'],
|
||||
}),
|
||||
).toMatchZod(z.literal('someValue'));
|
||||
});
|
||||
|
||||
test('should create enum array with string enums', () => {
|
||||
expect(
|
||||
parseEnum({
|
||||
enum: ['someValue', 'anotherValue'],
|
||||
}),
|
||||
).toMatchZod(z.enum(['someValue', 'anotherValue']));
|
||||
});
|
||||
test('should create union with mixed enums', () => {
|
||||
expect(
|
||||
parseEnum({
|
||||
enum: ['someValue', 57],
|
||||
}),
|
||||
).toMatchZod(z.union([z.literal('someValue'), z.literal(57)]));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseNot } from '../../src/parsers/parse-not';
|
||||
|
||||
describe('parseNot', () => {
|
||||
test('parseNot', () => {
|
||||
expect(
|
||||
parseNot(
|
||||
{
|
||||
not: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z
|
||||
.any()
|
||||
.refine(
|
||||
(value) => !z.string().safeParse(value).success,
|
||||
'Invalid input: Should NOT be valid against schema',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from '../../src/parsers/parse-schema';
|
||||
|
||||
describe('parseNullable', () => {
|
||||
test('parseSchema should not add default twice', () => {
|
||||
expect(
|
||||
parseSchema(
|
||||
{
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
default: null,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.string().nullable().default(null));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseNumber } from '../../src/parsers/parse-number';
|
||||
|
||||
describe('parseNumber', () => {
|
||||
test('should handle integer', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'integer',
|
||||
}),
|
||||
).toMatchZod(z.number().int());
|
||||
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'integer',
|
||||
multipleOf: 1,
|
||||
}),
|
||||
).toMatchZod(z.number().int());
|
||||
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
multipleOf: 1,
|
||||
}),
|
||||
).toMatchZod(z.number().int());
|
||||
});
|
||||
|
||||
test('should handle maximum with exclusiveMinimum', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
exclusiveMinimum: true,
|
||||
minimum: 2,
|
||||
}),
|
||||
).toMatchZod(z.number().gt(2));
|
||||
});
|
||||
|
||||
test('should handle maximum with exclusiveMinimum', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
minimum: 2,
|
||||
}),
|
||||
).toMatchZod(z.number().gte(2));
|
||||
});
|
||||
|
||||
test('should handle maximum with exclusiveMaximum', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
exclusiveMaximum: true,
|
||||
maximum: 2,
|
||||
}),
|
||||
).toMatchZod(z.number().lt(2));
|
||||
});
|
||||
|
||||
test('should handle numeric exclusiveMaximum', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
exclusiveMaximum: 2,
|
||||
}),
|
||||
).toMatchZod(z.number().lt(2));
|
||||
});
|
||||
|
||||
test('should accept errorMessage', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
format: 'int64',
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 2,
|
||||
multipleOf: 2,
|
||||
errorMessage: {
|
||||
format: 'ayy',
|
||||
multipleOf: 'lmao',
|
||||
exclusiveMinimum: 'deez',
|
||||
maximum: 'nuts',
|
||||
},
|
||||
}),
|
||||
).toMatchZod(z.number().int('ayy').multipleOf(2, 'lmao').gt(0, 'deez').lte(2, 'nuts'));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,904 @@
|
|||
/* eslint-disable n8n-local-rules/no-skipped-tests */
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
import { z, ZodError } from 'zod';
|
||||
|
||||
import { parseObject } from '../../src/parsers/parse-object';
|
||||
|
||||
describe('parseObject', () => {
|
||||
test('should handle with missing properties', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.record(z.any()));
|
||||
});
|
||||
|
||||
test('should handle with empty properties', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({}));
|
||||
});
|
||||
|
||||
test('With properties - should handle optional and required properties', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['myRequiredString'],
|
||||
properties: {
|
||||
myOptionalString: {
|
||||
type: 'string',
|
||||
},
|
||||
myRequiredString: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.object({ myOptionalString: z.string().optional(), myRequiredString: z.string() }),
|
||||
);
|
||||
});
|
||||
|
||||
test('With properties - should handle additionalProperties when set to false', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['myString'],
|
||||
properties: {
|
||||
myString: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ myString: z.string() }).strict());
|
||||
});
|
||||
|
||||
test('With properties - should handle additionalProperties when set to true', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['myString'],
|
||||
properties: {
|
||||
myString: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
additionalProperties: true,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ myString: z.string() }).catchall(z.any()));
|
||||
});
|
||||
|
||||
test('With properties - should handle additionalProperties when provided a schema', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['myString'],
|
||||
properties: {
|
||||
myString: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
additionalProperties: { type: 'number' },
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ myString: z.string() }).catchall(z.number()));
|
||||
});
|
||||
|
||||
test('Without properties - should handle additionalProperties when set to false', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.record(z.never()));
|
||||
});
|
||||
|
||||
test('Without properties - should handle additionalProperties when set to true', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.record(z.any()));
|
||||
});
|
||||
|
||||
test('Without properties - should handle additionalProperties when provided a schema', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'number' },
|
||||
},
|
||||
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.record(z.number()));
|
||||
});
|
||||
|
||||
test('Without properties - should include falsy defaults', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
s: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ s: z.string().default('') }));
|
||||
});
|
||||
|
||||
test('eh', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
required: ['c'],
|
||||
properties: {
|
||||
c: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z
|
||||
.object({ a: z.string() })
|
||||
.and(z.union([z.object({ b: z.string() }), z.object({ c: z.string() })])),
|
||||
);
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ a: z.string() }).and(z.union([z.object({ b: z.string() }), z.any()])));
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
required: ['c'],
|
||||
properties: {
|
||||
c: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.object({ a: z.string() }).and(
|
||||
z.any().superRefine((x, ctx) => {
|
||||
const schemas = [z.object({ b: z.string() }), z.object({ c: z.string() })];
|
||||
const errors = schemas.reduce<z.ZodError[]>(
|
||||
(errors, schema) =>
|
||||
((result) => (result.error ? [...errors, result.error] : errors))(
|
||||
schema.safeParse(x),
|
||||
),
|
||||
[],
|
||||
);
|
||||
if (schemas.length - errors.length !== 1) {
|
||||
ctx.addIssue({
|
||||
path: ctx.path,
|
||||
code: 'invalid_union',
|
||||
unionErrors: errors,
|
||||
message: 'Invalid input: Should pass single schema',
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.object({ a: z.string() }).and(
|
||||
z.any().superRefine((x, ctx) => {
|
||||
const schemas = [z.object({ b: z.string() }), z.any()];
|
||||
const errors = schemas.reduce<z.ZodError[]>(
|
||||
(errors, schema) =>
|
||||
((result) => (result.error ? [...errors, result.error] : errors))(
|
||||
schema.safeParse(x),
|
||||
),
|
||||
[],
|
||||
);
|
||||
if (schemas.length - errors.length !== 1) {
|
||||
ctx.addIssue({
|
||||
path: ctx.path,
|
||||
code: 'invalid_union',
|
||||
unionErrors: errors,
|
||||
message: 'Invalid input: Should pass single schema',
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
allOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
required: ['c'],
|
||||
properties: {
|
||||
c: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z
|
||||
.object({ a: z.string() })
|
||||
.and(z.intersection(z.object({ b: z.string() }), z.object({ c: z.string() }))),
|
||||
);
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
allOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.object({ a: z.string() }).and(z.intersection(z.object({ b: z.string() }), z.any())),
|
||||
);
|
||||
});
|
||||
|
||||
const run = (zodSchema: z.ZodTypeAny, data: unknown) => zodSchema.safeParse(data);
|
||||
|
||||
test('Functional tests - run', () => {
|
||||
expect(run(z.string(), 'hello')).toEqual({
|
||||
success: true,
|
||||
data: 'hello',
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - properties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z.object({ a: z.string(), b: z.number().optional() });
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
|
||||
expect(run(result, { a: 'hello' })).toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
a: 'hello',
|
||||
},
|
||||
});
|
||||
|
||||
expect(run(result, { a: 'hello', b: 123 })).toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
a: 'hello',
|
||||
b: 123,
|
||||
},
|
||||
});
|
||||
|
||||
expect(run(result, { b: 'hello', x: true })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'string',
|
||||
received: 'undefined',
|
||||
path: ['a'],
|
||||
message: 'Required',
|
||||
},
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'number',
|
||||
received: 'string',
|
||||
path: ['b'],
|
||||
message: 'Expected number, received string',
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - properties and additionalProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
additionalProperties: { type: 'boolean' },
|
||||
};
|
||||
|
||||
const expected = z.object({ a: z.string(), b: z.number().optional() }).catchall(z.boolean());
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
|
||||
expect(run(result, { b: 'hello', x: 'true' })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'string',
|
||||
received: 'undefined',
|
||||
path: ['a'],
|
||||
message: 'Required',
|
||||
},
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'number',
|
||||
received: 'string',
|
||||
path: ['b'],
|
||||
message: 'Expected number, received string',
|
||||
},
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'boolean',
|
||||
received: 'string',
|
||||
path: ['x'],
|
||||
message: 'Expected boolean, received string',
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - properties and single-item patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.object({ a: z.string(), b: z.number().optional() })
|
||||
.catchall(z.array(z.any()))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
if (key.match(new RegExp('\\\\.'))) {
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
|
||||
expect(run(result, { a: 'a', b: 2, '.': [] })).toEqual({
|
||||
success: true,
|
||||
data: { a: 'a', b: 2, '.': [] },
|
||||
});
|
||||
|
||||
expect(run(result, { a: 'a', b: 2, '.': '[]' })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'array',
|
||||
received: 'string',
|
||||
path: ['.'],
|
||||
message: 'Expected array, received string',
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - properties, additionalProperties and patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
additionalProperties: { type: 'boolean' },
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
'\\,': { type: 'array', minItems: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.object({ a: z.string(), b: z.number().optional() })
|
||||
.catchall(z.union([z.array(z.any()), z.array(z.any()).min(1), z.boolean()]))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
let evaluated = ['a', 'b'].includes(key);
|
||||
if (key.match(new RegExp('\\\\.'))) {
|
||||
evaluated = true;
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (key.match(new RegExp('\\\\,'))) {
|
||||
evaluated = true;
|
||||
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!evaluated) {
|
||||
const result = z.boolean().safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: 'Invalid input: must match catchall schema',
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
|
||||
test('Functional tests - additionalProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'boolean' },
|
||||
};
|
||||
|
||||
const expected = z.record(z.boolean());
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
|
||||
test('Functional tests - additionalProperties and patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'boolean' },
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
'\\,': { type: 'array', minItems: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.record(z.union([z.array(z.any()), z.array(z.any()).min(1), z.boolean()]))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
let evaluated = false;
|
||||
if (key.match(new RegExp('\\\\.'))) {
|
||||
evaluated = true;
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (key.match(new RegExp('\\\\,'))) {
|
||||
evaluated = true;
|
||||
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!evaluated) {
|
||||
const result = z.boolean().safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: 'Invalid input: must match catchall schema',
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
|
||||
expect(run(result, { x: true, '.': [], ',': [] })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
path: [','],
|
||||
code: 'custom',
|
||||
message: 'Invalid input: Key matching regex /,/ must match schema',
|
||||
params: {
|
||||
issues: [
|
||||
{
|
||||
code: 'too_small',
|
||||
minimum: 1,
|
||||
type: 'array',
|
||||
inclusive: true,
|
||||
exact: false,
|
||||
message: 'Array must contain at least 1 element(s)',
|
||||
path: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - single-item patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z.record(z.array(z.any())).superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
if (key.match(new RegExp('\\\\.'))) {
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
|
||||
test('Functional tests - patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
'\\,': { type: 'array', minItems: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.record(z.union([z.array(z.any()), z.array(z.any()).min(1)]))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
if (key.match(new RegExp('\\.'))) {
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (key.match(new RegExp('\\,'))) {
|
||||
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(run(result, { '.': [] })).toEqual({
|
||||
success: true,
|
||||
data: { '.': [] },
|
||||
});
|
||||
|
||||
expect(run(result, { ',': [] })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
path: [','],
|
||||
code: 'custom',
|
||||
message: 'Invalid input: Key matching regex /,/ must match schema',
|
||||
params: {
|
||||
issues: [
|
||||
{
|
||||
code: 'too_small',
|
||||
minimum: 1,
|
||||
type: 'array',
|
||||
inclusive: true,
|
||||
exact: false,
|
||||
message: 'Array must contain at least 1 element(s)',
|
||||
path: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
|
||||
test('Functional tests - patternProperties and properties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
'\\,': { type: 'array', minItems: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.object({ a: z.string(), b: z.number().optional() })
|
||||
.catchall(z.union([z.array(z.any()), z.array(z.any()).min(1)]))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
if (key.match(new RegExp('\\.'))) {
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (key.match(new RegExp('\\,'))) {
|
||||
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseOneOf } from '../../src/parsers/parse-one-of';
|
||||
|
||||
describe('parseOneOf', () => {
|
||||
test('should create a union from two or more schemas', () => {
|
||||
expect(
|
||||
parseOneOf(
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{ type: 'number' },
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.any().superRefine((x, ctx) => {
|
||||
const schemas = [z.string(), z.number()];
|
||||
const errors = schemas.reduce<z.ZodError[]>(
|
||||
(errors, schema) =>
|
||||
((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)),
|
||||
[],
|
||||
);
|
||||
if (schemas.length - errors.length !== 1) {
|
||||
ctx.addIssue({
|
||||
path: ctx.path,
|
||||
code: 'invalid_union',
|
||||
unionErrors: errors,
|
||||
message: 'Invalid input: Should pass single schema',
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should extract a single schema', () => {
|
||||
expect(parseOneOf({ oneOf: [{ type: 'string' }] }, { path: [], seen: new Map() })).toMatchZod(
|
||||
z.string(),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return z.any() if array is empty', () => {
|
||||
expect(parseOneOf({ oneOf: [] }, { path: [], seen: new Map() })).toMatchZod(z.any());
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from '../../src/parsers/parse-schema';
|
||||
|
||||
describe('parseSchema', () => {
|
||||
test('should be usable without providing refs', () => {
|
||||
expect(parseSchema({ type: 'string' })).toMatchZod(z.string());
|
||||
});
|
||||
|
||||
test('should return a seen and processed ref', () => {
|
||||
const seen = new Map();
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prop: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(parseSchema(schema, { seen, path: [] }));
|
||||
expect(parseSchema(schema, { seen, path: [] }));
|
||||
});
|
||||
|
||||
test('should be possible to describe a readonly schema', () => {
|
||||
expect(parseSchema({ type: 'string', readOnly: true })).toMatchZod(z.string().readonly());
|
||||
});
|
||||
|
||||
test('should handle nullable', () => {
|
||||
expect(
|
||||
parseSchema(
|
||||
{
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.string().nullable());
|
||||
});
|
||||
|
||||
test('should handle enum', () => {
|
||||
expect(parseSchema({ enum: ['someValue', 57] })).toMatchZod(
|
||||
z.union([z.literal('someValue'), z.literal(57)]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle multiple type', () => {
|
||||
expect(parseSchema({ type: ['string', 'number'] })).toMatchZod(
|
||||
z.union([z.string(), z.number()]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle if-then-else type', () => {
|
||||
expect(
|
||||
parseSchema({
|
||||
if: { type: 'string' },
|
||||
then: { type: 'number' },
|
||||
else: { type: 'boolean' },
|
||||
}),
|
||||
).toMatchZod(
|
||||
z.union([z.number(), z.boolean()]).superRefine((value, ctx) => {
|
||||
const result = z.string().safeParse(value).success
|
||||
? z.number().safeParse(value)
|
||||
: z.boolean().safeParse(value);
|
||||
if (!result.success) {
|
||||
result.error.errors.forEach((error) => ctx.addIssue(error));
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle anyOf', () => {
|
||||
expect(
|
||||
parseSchema({
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{ type: 'number' },
|
||||
],
|
||||
}),
|
||||
).toMatchZod(z.union([z.string(), z.number()]));
|
||||
});
|
||||
|
||||
test('should handle oneOf', () => {
|
||||
expect(
|
||||
parseSchema({
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{ type: 'number' },
|
||||
],
|
||||
}),
|
||||
).toMatchZod(
|
||||
z.any().superRefine((x, ctx) => {
|
||||
const schemas = [z.string(), z.number()];
|
||||
const errors = schemas.reduce<z.ZodError[]>(
|
||||
(errors, schema) =>
|
||||
((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)),
|
||||
[],
|
||||
);
|
||||
if (schemas.length - errors.length !== 1) {
|
||||
ctx.addIssue({
|
||||
path: ctx.path,
|
||||
code: 'invalid_union',
|
||||
unionErrors: errors,
|
||||
message: 'Invalid input: Should pass single schema',
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,152 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseString } from '../../src/parsers/parse-string';
|
||||
|
||||
describe('parseString', () => {
|
||||
const run = (schema: z.ZodString, data: unknown) => schema.safeParse(data);
|
||||
|
||||
test('DateTime format', () => {
|
||||
const datetime = '2018-11-13T20:20:39Z';
|
||||
|
||||
const code = parseString({
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
errorMessage: { format: 'hello' },
|
||||
});
|
||||
|
||||
expect(code).toMatchZod(z.string().datetime({ offset: true, message: 'hello' }));
|
||||
|
||||
expect(run(code, datetime)).toEqual({ success: true, data: datetime });
|
||||
});
|
||||
|
||||
test('email', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
}),
|
||||
).toMatchZod(z.string().email());
|
||||
});
|
||||
|
||||
test('ip', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'ip',
|
||||
}),
|
||||
).toMatchZod(z.string().ip());
|
||||
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'ipv6',
|
||||
}),
|
||||
).toMatchZod(z.string().ip({ version: 'v6' }));
|
||||
});
|
||||
|
||||
test('uri', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
}),
|
||||
).toMatchZod(z.string().url());
|
||||
});
|
||||
|
||||
test('uuid', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
}),
|
||||
).toMatchZod(z.string().uuid());
|
||||
});
|
||||
|
||||
test('time', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'time',
|
||||
}),
|
||||
).toMatchZod(z.string().time());
|
||||
});
|
||||
|
||||
test('date', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
}),
|
||||
).toMatchZod(z.string().date());
|
||||
});
|
||||
|
||||
test('duration', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'duration',
|
||||
}),
|
||||
).toMatchZod(z.string().duration());
|
||||
});
|
||||
|
||||
test('base64', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
contentEncoding: 'base64',
|
||||
}),
|
||||
).toMatchZod(z.string().base64());
|
||||
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
contentEncoding: 'base64',
|
||||
errorMessage: {
|
||||
contentEncoding: 'x',
|
||||
},
|
||||
}),
|
||||
).toMatchZod(z.string().base64('x'));
|
||||
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
}),
|
||||
).toMatchZod(z.string().base64());
|
||||
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
errorMessage: {
|
||||
format: 'x',
|
||||
},
|
||||
}),
|
||||
).toMatchZod(z.string().base64('x'));
|
||||
});
|
||||
|
||||
test('should accept errorMessage', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'ipv4',
|
||||
pattern: 'x',
|
||||
minLength: 1,
|
||||
maxLength: 2,
|
||||
errorMessage: {
|
||||
format: 'ayy',
|
||||
pattern: 'lmao',
|
||||
minLength: 'deez',
|
||||
maxLength: 'nuts',
|
||||
},
|
||||
}),
|
||||
).toMatchZod(
|
||||
z
|
||||
.string()
|
||||
.ip({ version: 'v4', message: 'ayy' })
|
||||
.regex(new RegExp('x'), 'lmao')
|
||||
.min(1, 'deez')
|
||||
.max(2, 'nuts'),
|
||||
);
|
||||
});
|
||||
});
|
15
packages/@n8n/json-schema-to-zod/test/utils/half.test.ts
Normal file
15
packages/@n8n/json-schema-to-zod/test/utils/half.test.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { half } from '../../src/utils/half';
|
||||
|
||||
describe('half', () => {
|
||||
test('half', () => {
|
||||
const [a, b] = half(['A', 'B', 'C', 'D', 'E']);
|
||||
|
||||
if (1 < 0) {
|
||||
// type should be string
|
||||
a[0].endsWith('');
|
||||
}
|
||||
|
||||
expect(a).toEqual(['A', 'B']);
|
||||
expect(b).toEqual(['C', 'D', 'E']);
|
||||
});
|
||||
});
|
27
packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts
Normal file
27
packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { omit } from '../../src/utils/omit';
|
||||
|
||||
describe('omit', () => {
|
||||
test('omit', () => {
|
||||
const input = {
|
||||
a: true,
|
||||
b: true,
|
||||
};
|
||||
|
||||
omit(
|
||||
input,
|
||||
'b',
|
||||
// @ts-expect-error
|
||||
'c',
|
||||
);
|
||||
|
||||
const output = omit(input, 'b');
|
||||
|
||||
// @ts-expect-error
|
||||
output.b;
|
||||
|
||||
expect(output.a).toBe(true);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(output.b).toBeUndefined();
|
||||
});
|
||||
});
|
11
packages/@n8n/json-schema-to-zod/tsconfig.cjs.json
Normal file
11
packages/@n8n/json-schema-to-zod/tsconfig.cjs.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"outDir": "dist/cjs",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
12
packages/@n8n/json-schema-to-zod/tsconfig.esm.json
Normal file
12
packages/@n8n/json-schema-to-zod/tsconfig.esm.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "es2020",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist/esm",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
12
packages/@n8n/json-schema-to-zod/tsconfig.json
Normal file
12
packages/@n8n/json-schema-to-zod/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": ["../../../tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"baseUrl": "src",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
11
packages/@n8n/json-schema-to-zod/tsconfig.types.json
Normal file
11
packages/@n8n/json-schema-to-zod/tsconfig.types.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist/types",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
|
@ -394,6 +394,18 @@ importers:
|
|||
specifier: ^0.0.3
|
||||
version: 0.0.3(patch_hash=3i7wecddkama6vhpu5o37g24u4)
|
||||
|
||||
packages/@n8n/json-schema-to-zod:
|
||||
devDependencies:
|
||||
'@types/json-schema':
|
||||
specifier: ^7.0.15
|
||||
version: 7.0.15
|
||||
'@types/node':
|
||||
specifier: ^18.16.16
|
||||
version: 18.16.16
|
||||
zod:
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.8
|
||||
|
||||
packages/@n8n/nodes-langchain:
|
||||
dependencies:
|
||||
'@aws-sdk/client-sso-oidc':
|
||||
|
|
Loading…
Reference in a new issue