Skip to content

Commit e5c76c3

Browse files
committed
Fixed union handling with default values
1 parent dfa8925 commit e5c76c3

File tree

5 files changed

+106
-9
lines changed

5 files changed

+106
-9
lines changed

src/lib/errors.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { SchemaShape } from './jsonSchema/schemaShape.js';
2-
import { pathExists, setPaths, traversePath, traversePaths } from './traversal.js';
2+
import { pathExists, setPaths, traversePath, traversePaths, type PathData } from './traversal.js';
33
import { mergePath } from './stringPath.js';
44
import type { ValidationErrors } from './superValidate.js';
55
import { defaultTypes, defaultValue, type SchemaFieldType } from './jsonSchema/schemaDefaults.js';
@@ -168,6 +168,7 @@ export function replaceInvalidDefaults<T extends Record<string, unknown>>(
168168

169169
//#region Types
170170

171+
// TODO: Memoize Types with _schema?
171172
const Types = defaultTypes(_schema);
172173

173174
function Types_correctValue(dataValue: unknown, defValue: unknown, type: SchemaFieldType) {
@@ -249,11 +250,7 @@ export function replaceInvalidDefaults<T extends Record<string, unknown>>(
249250
//#region Defaults
250251

251252
function Defaults_traverseAndReplace(
252-
defaultPath: {
253-
path: (string | number | symbol)[];
254-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
255-
value: any;
256-
},
253+
defaultPath: Partial<PathData>,
257254
traversingErrors = false
258255
): void {
259256
const currentPath = defaultPath.path;

src/lib/jsonSchema/schemaDefaults.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,17 +244,24 @@ type SchemaTypeObject = {
244244
[Key in Exclude<string, '_types' | '_items'>]: SchemaTypeObject;
245245
} & SchemaFieldType;
246246

247-
function _defaultTypes(schema: JSONSchema, isOptional: boolean, path: string[]) {
247+
function _defaultTypes(schema: JSONSchema, isOptional: boolean, path: string[]): SchemaTypeObject {
248248
if (!schema) {
249249
throw new SchemaError('Schema was undefined', path);
250250
}
251251

252252
const info = schemaInfo(schema, isOptional, path);
253253

254-
const output = {
254+
let output = {
255255
__types: info.types
256256
} as SchemaTypeObject;
257257

258+
if (info.union) {
259+
output = merge(
260+
output,
261+
...info.union.map((u) => _defaultTypes(u, info.isOptional, path))
262+
) as SchemaTypeObject;
263+
}
264+
258265
//if (schema.type == 'object') console.log('--- OBJECT ---'); //debug
259266
//else console.dir({ path, info }, { depth: 10 }); //debug
260267

src/tests/logOnlyFails.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { type TestContext } from 'vitest';
2+
3+
const loggers = ['log', 'debug', 'trace', 'info', 'warn', 'error'] as const;
4+
5+
type LogFunction = (...args: unknown[]) => void;
6+
type LoggedInvocation = [LogFunction, unknown[]];
7+
8+
export const logOnlyFails = (ctx: TestContext) => {
9+
const logs: LoggedInvocation[] = [];
10+
11+
const original: Record<string, LogFunction> = {};
12+
for (const logger of loggers) {
13+
original[logger] = console[logger];
14+
console[logger] = (...args) => logs.push([original[logger], args]);
15+
}
16+
17+
ctx.onTestFailed(() => {
18+
for (const [logger, data] of logs) {
19+
logger.call(console, ...data);
20+
}
21+
});
22+
23+
return () => {
24+
for (const logger of loggers) {
25+
console[logger] = original[logger];
26+
}
27+
};
28+
};

src/tests/superValidate.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,10 @@ describe('Arktype', () => {
443443
expect(form.valid).toBe(false);
444444
expect(form.errors.id).toBeTruthy();
445445
});
446+
});
447+
});
446448

447-
describe('with optional properties', () => {
449+
describe('with optional properties', () => {
448450
const schema = type({
449451
name: 'string',
450452
'email?': 'string.email',

src/tests/zod4Union.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,69 @@ describe('New discriminatedUnion features', () => {
5656
assert(form.data.message === 'FAIL');
5757
assert(form.data.code === 401);
5858
});
59+
60+
test('A complicated union', async () => {
61+
const ZodSchema2 = z.discriminatedUnion('type', [
62+
z.object({
63+
type: z.literal('empty')
64+
}),
65+
z.object({
66+
type: z.literal('additional'),
67+
additional: z.discriminatedUnion('type', [
68+
z.object({
69+
type: z.literal('poBox'),
70+
name: z
71+
.string()
72+
.min(1, 'min len')
73+
.max(10, 'max len')
74+
.default(null as unknown as string)
75+
}),
76+
z.object({
77+
type: z.literal('none')
78+
})
79+
])
80+
})
81+
]);
82+
83+
const FormSchema = zod(ZodSchema2);
84+
type FormSchema = (typeof FormSchema)['defaults'];
85+
86+
{
87+
const data = {
88+
type: 'additional',
89+
additional: {
90+
// @ts-expect-error Testing with invalid data
91+
type: 123,
92+
name: ''
93+
}
94+
} satisfies FormSchema;
95+
96+
// @ts-expect-error Testing with invalid data
97+
const form = await validate(data, FormSchema);
98+
expect(form.valid).toBe(false);
99+
expect(form.data).toEqual({
100+
type: 'additional',
101+
additional: {
102+
type: 'none',
103+
name: ''
104+
}
105+
});
106+
}
107+
108+
{
109+
const data = {
110+
type: 'additional',
111+
additional: {
112+
type: 'poBox',
113+
name: ''
114+
}
115+
} satisfies FormSchema;
116+
117+
const form = await validate(data, FormSchema);
118+
expect(form.valid).toBe(false);
119+
expect(form.data).toEqual(data);
120+
}
121+
});
59122
});
60123

61124
describe('Default discriminated union values 1', () => {

0 commit comments

Comments
 (0)