mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -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
|
specifier: ^0.0.3
|
||||||
version: 0.0.3(patch_hash=3i7wecddkama6vhpu5o37g24u4)
|
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:
|
packages/@n8n/nodes-langchain:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-sso-oidc':
|
'@aws-sdk/client-sso-oidc':
|
||||||
|
|
Loading…
Reference in a new issue