Skip to content

Commit fc1ef6d

Browse files
committed
WIP: Special case for URL booleans vs FormData.
1 parent 53bcd5c commit fc1ef6d

File tree

2 files changed

+146
-13
lines changed

2 files changed

+146
-13
lines changed

src/lib/formData.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function parseSearchParams<T extends Record<string, unknown>>(
130130
convert.append(key, value);
131131
}
132132

133-
const output = parseFormData(convert, schemaData, options);
133+
const output = parseFormData(convert, schemaData, options, true);
134134

135135
// Set posted to false since it's a URL
136136
output.posted = false;
@@ -140,7 +140,8 @@ export function parseSearchParams<T extends Record<string, unknown>>(
140140
export function parseFormData<T extends Record<string, unknown>>(
141141
formData: FormData,
142142
schemaData: JSONSchema,
143-
options?: SuperValidateOptions<T>
143+
options?: SuperValidateOptions<T>,
144+
fromURL = false
144145
): ParsedData {
145146
function tryParseSuperJson() {
146147
if (formData.has('__superform_json')) {
@@ -183,15 +184,16 @@ export function parseFormData<T extends Record<string, unknown>>(
183184
? { id, data, posted: true }
184185
: {
185186
id,
186-
data: _parseFormData(formData, schemaData, options),
187+
data: _parseFormData(formData, schemaData, options, fromURL),
187188
posted: true
188189
};
189190
}
190191

191192
function _parseFormData<T extends Record<string, unknown>>(
192193
formData: FormData,
193194
schema: JSONSchema,
194-
options?: SuperValidateOptions<T>
195+
options?: SuperValidateOptions<T>,
196+
fromURL = false
195197
) {
196198
const output: Record<string, unknown> = {};
197199

@@ -221,11 +223,13 @@ function _parseFormData<T extends Record<string, unknown>>(
221223
);
222224
}
223225

224-
function parseSingleEntry(key: string, entry: FormDataEntryValue, info: SchemaInfo) {
226+
function parseSingleEntry(key: string, entry: FormDataEntryValue | undefined, info: SchemaInfo) {
225227
if (options?.preprocessed && options.preprocessed.includes(key as keyof T)) {
226228
return entry;
227229
}
228230

231+
console.log(`Parsing entry for key "${key}":`, entry); //debug
232+
229233
if (entry && typeof entry !== 'string') {
230234
const allowFiles = legacyMode ? options?.allowFiles === true : options?.allowFiles !== false;
231235
return !allowFiles ? undefined : entry.size ? entry : info.isNullable ? null : undefined;
@@ -237,7 +241,7 @@ function _parseFormData<T extends Record<string, unknown>>(
237241

238242
let [type] = info.types;
239243

240-
if (!info.types.length && info.schema.enum) {
244+
if (entry && !info.types.length && info.schema.enum) {
241245
// Special case for Typescript enums
242246
// If the entry is an integer, parse it as such, otherwise string
243247
if (info.schema.enum.includes(entry)) type = 'string';
@@ -246,7 +250,7 @@ function _parseFormData<T extends Record<string, unknown>>(
246250
}
247251
}
248252

249-
return parseFormDataEntry(key, entry, type ?? 'any', info);
253+
return parseFormDataEntry(key, entry, type ?? 'any', info, fromURL);
250254
}
251255

252256
const defaultPropertyType =
@@ -308,17 +312,18 @@ function _parseFormData<T extends Record<string, unknown>>(
308312

309313
function parseFormDataEntry(
310314
key: string,
311-
value: string,
315+
value: string | undefined,
312316
type: Exclude<SchemaType, 'null'>,
313-
info: SchemaInfo
317+
info: SchemaInfo,
318+
fromURL = false
314319
): unknown {
315-
//console.log(`Parsing FormData ${key} (${type}): "${value}"`, info); //debug
320+
console.log(`Parsing FormData ${key} (${type}): ${value ? `"${value}"` : 'undefined'}`, info); //debug
316321

317322
if (!value) {
318-
//console.log(`No FormData for "${key}" (${type}).`, info); //debug
323+
console.log(`No FormData for "${key}" (${type}).`, info); //debug
319324

320-
// Special case for booleans with default value true
321-
if (type == 'boolean' && info.isOptional && info.schema.default === true) {
325+
// Special case for booleans with default value true, *when posted* (not from URL)
326+
if (!fromURL && type == 'boolean' && info.isOptional && info.schema.default === true) {
322327
return false;
323328
}
324329

src/tests/issue-633.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { superValidate } from '$lib/superValidate.js';
3+
import { zod } from '$lib/adapters/zod.js';
4+
import { z } from 'zod/v3';
5+
6+
describe('Issue #633 - Boolean default value with URL vs FormData validation', () => {
7+
it('should return true for boolean field with .default(true) when validating empty URL', async () => {
8+
// Schema with boolean default set to true
9+
const formSchema = z.object({
10+
orderby: z.boolean().default(true)
11+
});
12+
13+
// Create URL with no searchParams
14+
const url = new URL('http://test.com');
15+
16+
// Validate URL with schema
17+
const form = await superValidate(url, zod(formSchema));
18+
19+
// The boolean field should default to true, not false
20+
expect(form.data.orderby).toBe(true);
21+
});
22+
23+
it('should respect explicit false value in URL searchParams', async () => {
24+
// Schema with boolean default set to true
25+
const formSchema = z.object({
26+
orderby: z.boolean().default(true)
27+
});
28+
29+
// Create URL with orderby=false
30+
const url = new URL('http://test.com?orderby=false');
31+
32+
// Validate URL with schema
33+
const form = await superValidate(url, zod(formSchema));
34+
35+
// The explicit false value should be preserved
36+
expect(form.data.orderby).toBe(false);
37+
});
38+
39+
it('should use true default when URL has other params but not orderby', async () => {
40+
// Schema with boolean default set to true
41+
const formSchema = z.object({
42+
orderby: z.boolean().default(true),
43+
search: z.string().optional()
44+
});
45+
46+
// Create URL with other params but not orderby
47+
const url = new URL('http://test.com?search=test');
48+
49+
// Validate URL with schema
50+
const form = await superValidate(url, zod(formSchema));
51+
52+
// The boolean field should default to true
53+
expect(form.data.orderby).toBe(true);
54+
expect(form.data.search).toBe('test');
55+
});
56+
57+
// FormData tests - different behavior expected
58+
describe('FormData validation', () => {
59+
it('should return false for boolean field with .default(true) when validating empty FormData', async () => {
60+
// Schema with boolean default set to true
61+
const formSchema = z.object({
62+
orderby: z.boolean().default(true)
63+
});
64+
65+
// Create empty FormData
66+
const formData = new FormData();
67+
68+
// Validate FormData with schema
69+
const form = await superValidate(formData, zod(formSchema));
70+
71+
// For FormData, missing boolean should return false (HTML form behavior)
72+
expect(form.data.orderby).toBe(false);
73+
});
74+
75+
it('should respect explicit false value in FormData', async () => {
76+
// Schema with boolean default set to true
77+
const formSchema = z.object({
78+
orderby: z.boolean().default(true)
79+
});
80+
81+
// Create FormData with orderby=false
82+
const formData = new FormData();
83+
formData.set('orderby', 'false');
84+
85+
// Validate FormData with schema
86+
const form = await superValidate(formData, zod(formSchema));
87+
88+
// The explicit false value should be preserved
89+
expect(form.data.orderby).toBe(false);
90+
});
91+
92+
it('should return true for explicit true value in FormData', async () => {
93+
// Schema with boolean default set to true
94+
const formSchema = z.object({
95+
orderby: z.boolean().default(true)
96+
});
97+
98+
// Create FormData with orderby=true
99+
const formData = new FormData();
100+
formData.set('orderby', 'true');
101+
102+
// Validate FormData with schema
103+
const form = await superValidate(formData, zod(formSchema));
104+
105+
// The explicit true value should be preserved
106+
expect(form.data.orderby).toBe(true);
107+
});
108+
109+
it('should return false when FormData has other fields but not orderby', async () => {
110+
// Schema with boolean default set to true
111+
const formSchema = z.object({
112+
orderby: z.boolean().default(true),
113+
search: z.string().optional()
114+
});
115+
116+
// Create FormData with other fields but not orderby
117+
const formData = new FormData();
118+
formData.set('search', 'test');
119+
120+
// Validate FormData with schema
121+
const form = await superValidate(formData, zod(formSchema));
122+
123+
// For FormData, missing boolean should return false (HTML form behavior)
124+
expect(form.data.orderby).toBe(false);
125+
expect(form.data.search).toBe('test');
126+
});
127+
});
128+
});

0 commit comments

Comments
 (0)