Skip to content

Commit 3224bd7

Browse files
authored
Refactor Validators (#35)
1 parent afd5368 commit 3224bd7

21 files changed

Lines changed: 903 additions & 340 deletions

.changeset/dry-cats-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"conformal": patch
3+
---
4+
5+
Align serialize false boolean behavior with coerceBoolean

.changeset/huge-planets-yawn.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"conformal": minor
3+
---
4+
5+
Add coerce functions
6+
7+
- Add `coerceString`, `coerceNumber`, `coerceBigint`, `coerceBoolean`, `coerceDate`, `coerceFile`, `coerceArray` functions
8+
- These utilities help convert form input values to their expected types
9+
- Essential for building custom schema implementations (like zod preproccessors or valibot transforms)

.changeset/many-beds-tap.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"conformal": minor
3+
---
4+
5+
Deprecate zod utilities
6+
7+
- Mark `conformal/zod` utilities as deprecated
8+
- Zod's `z.preprocess` returns a `ZodPipe` which doesn't allow method chaining, making these utilities less useful than expected
9+
- Zod utilities will be removed in the next major release
10+
- Users can migrate to using z.preprocess with the new coerce functions directly:
11+
12+
```typescript
13+
import * as z from "zod";
14+
import { coerceNumber } from "conformal";
15+
16+
z.preprocess(coerceNumber, z.number().min(5));
17+
```

.changeset/rich-feet-throw.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"conformal": minor
3+
---
4+
5+
Add valibot schemas
6+
7+
- Add `conformal/valibot` subpath with valibot utilities
8+
- Provides `string`, `number`, `boolean`, `date`, `bigint`, `picklist`, `file`, `array` schemas
9+
- Uses conformal's coerce functions for automatic form input preprocessing
10+
- Fully compatible with valibot and can be mixed with regular valibot schemas
11+
- Marked as experimental - API may change

AGENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ cd examples/svelte && npm i && npm run dev
1919

2020
## Public API
2121

22-
- `conformal`: `getPath`, `setPath`, `decode`, `parseFormData`, `serialize`; types: `PathsFromObject`, `Submission`
23-
- `conformal/zod`: `string`, `number`, `boolean`, `date`, `bigint`, `enum`, `file`, `url`, `email`, `object`, `array`
22+
- `conformal`: `getPath`, `setPath`, `decode`, `parseFormData`, `serialize`, `coerceString`, `coerceNumber`, `coerceBigint`, `coerceBoolean`, `coerceDate`, `coerceFile`, `coerceArray`; types: `PathsFromObject`, `Submission`
23+
- `conformal/valibot`: `string`, `number`, `boolean`, `date`, `bigint`, `picklist`, `file`, `array` (experimental)
24+
- `conformal/zod`: `string`, `number`, `boolean`, `date`, `bigint`, `enum`, `file`, `url`, `email`, `object`, `array` (deprecated)
2425

25-
Exports live in `src/index.ts` and `src/zod/index.ts`.
26+
Exports live in `src/index.ts`, `src/valibot/index.ts`, and `src/zod/index.ts`.
2627

2728
## Non‑negotiable invariants
2829

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Here's a quick example showing how Conformal handles form validation with a user
2828

2929
```typescript
3030
import { parseFormData } from "conformal";
31-
import * as z from "zod"; // Tip: Use conformal/zod for automatic form input preprocessing
31+
import * as z from "zod"; // Tip: Use conformal's coerce functions for form input preprocessing
3232

3333
const schema = z.object({
3434
name: z.string().min(2, "Name must be at least 2 characters"),
@@ -70,16 +70,26 @@ That's it! Conformal automatically handles FormData parsing, type coercion, and
7070
- **[`getPath`](src/README.md#getpath)** - Safely access nested values using dot/bracket notation
7171
- **[`setPath`](src/README.md#setpath)** - Immutably set nested values using dot/bracket notation
7272

73+
### Coerce Functions
74+
75+
- **[`coerceString`](src/README.md#coercestring)** - String handling
76+
- **[`coerceFile`](src/README.md#coercefile)** - File handling
77+
- **[`coerceNumber`](src/README.md#coercenumber)** - String to number coercion
78+
- **[`coerceBigint`](src/README.md#coercebigint)** - String to BigInt coercion
79+
- **[`coerceBoolean`](src/README.md#coerceboolean)** - String to boolean coercion
80+
- **[`coerceDate`](src/README.md#coercedate)** - String to Date coercion
81+
- **[`coerceArray`](src/README.md#coercearray)** - Coerce to array
82+
7383
### Types
7484

7585
- **[`Submission`](src/README.md#submission)** - Standardized submission result with success/error states
7686
- **[`PathsFromObject`](src/README.md#pathsfromobject)** - Type utility to extract all possible object paths
7787

78-
### Zod Utilities
88+
### Valibot Utilities
7989

8090
> ⚠️ **Experimental**: These utilities are still in development and may change.
8191
82-
- **[Zod Field Schemas](src/zod/README.md#field-schemas)** - Zod schemas with automatic form input preprocessing
92+
- **[Valibot Field Schemas](src/valibot/README.md#field-schemas)** - Valibot schemas with automatic form input preprocessing
8393

8494
## License
8595

package-lock.json

Lines changed: 26 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,9 @@
1616
"types": "./dist/index.d.ts",
1717
"main": "./dist/index.js",
1818
"exports": {
19-
".": {
20-
"types": "./dist/index.d.ts",
21-
"default": "./dist/index.js"
22-
},
23-
"./zod": {
24-
"types": "./dist/zod/index.d.ts",
25-
"default": "./dist/zod/index.js"
26-
}
19+
".": "./dist/index.js",
20+
"./valibot": "./dist/valibot/index.js",
21+
"./zod": "./dist/zod/index.js"
2722
},
2823
"scripts": {
2924
"build": "tsc",
@@ -51,12 +46,18 @@
5146
"@types/node": "^24.5.2",
5247
"prettier": "^3.6.2",
5348
"typescript": "^5.9.2",
54-
"vitest": "^3.2.4"
49+
"valibot": "^1.1.0",
50+
"vitest": "^3.2.4",
51+
"zod": "^4.1.11"
5552
},
5653
"peerDependencies": {
54+
"valibot": "^1.0.0",
5755
"zod": "^4.0.0"
5856
},
5957
"peerDependenciesMeta": {
58+
"valibot": {
59+
"optional": true
60+
},
6061
"zod": {
6162
"optional": true
6263
}

src/README.md

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22

33
### Table of Contents
44

5-
- [Functions](#functions)
5+
- [Core Functions](#core-functions)
66
- [parseFormData](#parseformdata)
77
- [decode](#decode)
88
- [serialize](#serialize)
99
- [getPath](#getpath)
1010
- [setPath](#setpath)
11+
- [Coerce Functions](#coerce-functions)
12+
- [coerceString](#coercestring)
13+
- [coerceNumber](#coercenumber)
14+
- [coerceBigint](#coercebigint)
15+
- [coerceBoolean](#coerceboolean)
16+
- [coerceDate](#coercedate)
17+
- [coerceFile](#coercefile)
18+
- [coerceArray](#coercearray)
1119
- [Types](#types)
1220
- [Submission](#submission)
1321
- [PathsFromObject](#pathsfromobject)
1422

15-
## Functions
23+
## Core Functions
1624

1725
### parseFormData
1826

@@ -107,6 +115,107 @@ const newObj = setPath({ a: { b: { c: [] } } }, "a.b.c[1]", "hey");
107115
// Returns { a: { b: { c: [<empty>, 'hey'] } } }
108116
```
109117

118+
## Coerce Functions
119+
120+
The coerce functions provide utilities for converting form input values to their expected types. These functions are essential for building custom schema implementations (like zod or valibot schemas) where you need to transform string-based form data into proper JavaScript types before validation. All coerce functions handle empty strings by returning `undefined` and pass through non-matching types unchanged, making them safe to use in schema transformation pipelines.
121+
122+
### coerceString
123+
124+
Converts string input to a string value, returning `undefined` for empty strings.
125+
126+
```typescript
127+
import { coerceString } from "conformal";
128+
129+
console.log(coerceString("hello")); // "hello"
130+
console.log(coerceString("")); // undefined
131+
console.log(coerceString(123)); // 123 (unchanged)
132+
```
133+
134+
### coerceNumber
135+
136+
Converts string input to a number, returning `undefined` for empty or whitespace-only strings.
137+
138+
```typescript
139+
import { coerceNumber } from "conformal";
140+
141+
console.log(coerceNumber("42")); // 42
142+
console.log(coerceNumber("3.14")); // 3.14
143+
console.log(coerceNumber("")); // undefined
144+
console.log(coerceNumber(" ")); // undefined
145+
console.log(coerceNumber("abc")); // NaN
146+
```
147+
148+
### coerceBigint
149+
150+
Converts string input to a BigInt, returning `undefined` for empty or whitespace-only strings. Returns the original string if conversion fails.
151+
152+
```typescript
153+
import { coerceBigint } from "conformal";
154+
155+
console.log(coerceBigint("42")); // 42n
156+
console.log(coerceBigint("9007199254740991")); // 9007199254740991n
157+
console.log(coerceBigint("")); // undefined
158+
console.log(coerceBigint("abc")); // "abc" (unchanged)
159+
```
160+
161+
### coerceBoolean
162+
163+
Converts string input to a boolean based on common truthy/falsy string values.
164+
165+
```typescript
166+
import { coerceBoolean } from "conformal";
167+
168+
console.log(coerceBoolean("true")); // true
169+
console.log(coerceBoolean("on")); // true
170+
console.log(coerceBoolean("1")); // true
171+
console.log(coerceBoolean("yes")); // true
172+
console.log(coerceBoolean("false")); // false
173+
console.log(coerceBoolean("off")); // false
174+
console.log(coerceBoolean("0")); // false
175+
console.log(coerceBoolean("no")); // false
176+
console.log(coerceBoolean("")); // undefined
177+
console.log(coerceBoolean("maybe")); // "maybe" (unchanged)
178+
```
179+
180+
### coerceDate
181+
182+
Converts string input to a Date object, returning `undefined` for empty strings. Returns the original string if the date is invalid.
183+
184+
```typescript
185+
import { coerceDate } from "conformal";
186+
187+
console.log(coerceDate("2023-01-01")); // Date object
188+
console.log(coerceDate("")); // undefined
189+
console.log(coerceDate("invalid-date")); // "invalid-date" (unchanged)
190+
```
191+
192+
### coerceFile
193+
194+
Handles File objects, returning `undefined` for empty files (size 0).
195+
196+
```typescript
197+
import { coerceFile } from "conformal";
198+
199+
const emptyFile = new File([], "test.txt");
200+
const file = new File(["content"], "test.txt");
201+
202+
console.log(coerceFile(emptyFile)); // undefined
203+
console.log(coerceFile(file)); // File object
204+
console.log(coerceFile("not-a-file")); // "not-a-file" (unchanged)
205+
```
206+
207+
### coerceArray
208+
209+
Converts any input to an array. Empty strings become empty arrays, arrays pass through unchanged, and other values are wrapped in an array.
210+
211+
```typescript
212+
import { coerceArray } from "conformal";
213+
214+
console.log(coerceArray("")); // []
215+
console.log(coerceArray([1, 2, 3])); // [1, 2, 3]
216+
console.log(coerceArray("hello")); // ["hello"]
217+
```
218+
110219
## Types
111220

112221
### Submission

0 commit comments

Comments
 (0)