Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/all-mangos-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"conformal": patch
---

Align coerceNumber invalid input handling
5 changes: 5 additions & 0 deletions .changeset/gold-numbers-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"conformal": patch
---

Refactored valibot utilities from schemas to composable coercion pipes for form input preprocessing.
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ cd examples/svelte && npm i && npm run dev
## Public API

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

Exports live in `src/index.ts`, `src/valibot/index.ts`, and `src/zod/index.ts`.
Expand Down Expand Up @@ -53,6 +53,6 @@ npx vitest run test/parse.test.ts

## Quick playbooks

- Add a Zod helper: edit `src/zod/schemas.ts`, re-export in `src/zod/index.ts`, add tests in `test/zod/`, update README's Zod section.
- Add a Valibot coercion pipe: edit `src/valibot/coerce.ts`, re-export in `src/valibot/index.ts`, add tests in `test/valibot/coerce.test.ts`, update README's Valibot section.
- Fix path bug: add failing test in `test/path.test.ts`, update `src/path.ts` (immutability + guards), run full checks.
- Public API change: update `src/index.ts`, tests, README; keep exports stable.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ That's it! Conformal automatically handles FormData parsing, type coercion, and
- **[`serialize`](src/README.md#serialize)** - Transform typed values back to form-compatible strings
- **[`getPath`](src/README.md#getpath)** - Safely access nested values using dot/bracket notation
- **[`setPath`](src/README.md#setpath)** - Immutably set nested values using dot/bracket notation
- **[`coerceX`](src/README.md#coerce-functions)** - A set of coercion functions for building custom schemas
- **[`coerceX`](src/README.md#coerce-functions)** - A set of coercion functions for use with schema libraries

### Types

Expand All @@ -80,7 +80,7 @@ That's it! Conformal automatically handles FormData parsing, type coercion, and

> ⚠️ **Experimental**: These utilities are still in development and may change.

- **[Valibot Field Schemas](src/valibot/README.md#field-schemas)** - Valibot schemas with automatic form input preprocessing
- **[Coercion Pipes](src/valibot/README.md#coercion-pipes)** - Pipes for automatic form input preprocessing

## License

Expand Down
5 changes: 3 additions & 2 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const newObj = setPath({ a: { b: { c: [] } } }, "a.b.c[1]", "hey");

## Coerce Functions

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.
The coerce functions provide utilities for converting form input values to their expected types. These functions are essential for building schema library (e.g. zod) utilities 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.

### coerceString

Expand All @@ -142,7 +142,7 @@ console.log(coerceNumber("42")); // 42
console.log(coerceNumber("3.14")); // 3.14
console.log(coerceNumber("")); // undefined
console.log(coerceNumber(" ")); // undefined
console.log(coerceNumber("abc")); // NaN
console.log(coerceNumber("abc")); // "abc" (unchanged)
```

### coerceBigint
Expand All @@ -155,6 +155,7 @@ import { coerceBigint } from "conformal";
console.log(coerceBigint("42")); // 42n
console.log(coerceBigint("9007199254740991")); // 9007199254740991n
console.log(coerceBigint("")); // undefined
console.log(coerceNumber(" ")); // undefined
console.log(coerceBigint("abc")); // "abc" (unchanged)
```

Expand Down
3 changes: 2 additions & 1 deletion src/coerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export function coerceNumber(input: unknown): unknown {
if (input.trim() === "") {
return undefined;
}
return Number(input);
const number = Number(input);
return Number.isNaN(number) ? input : number;
}

export function coerceBigint(input: unknown): unknown {
Expand Down
25 changes: 13 additions & 12 deletions src/valibot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,25 @@

The Valibot Utilities are provided under the `conformal/valibot` subpath. Valibot is an optional peer dependency, so you can freely choose another Standard Schema library if you prefer without depending on Valibot.

## Field Schemas
## Coercion Pipes

These field schemas are preprocessing wrappers that handle common form input patterns automatically by using conformal's set of coerce functions internally. They convert empty strings to `undefined`, coerce string inputs to appropriate types (numbers, dates, booleans), and handle `File` objects. They're fully compatible with Valibot and can be mixed with regular Valibot schemas.
These coercion pipes handle the conversion from form input values to rich JS types. They convert empty strings to `undefined`, coerce string inputs to appropriate types (numbers, dates, booleans), and handle `File` objects. They're composable pipes that can be combined with any valibot validation schema using `v.pipe()`.

```typescript
import * as vf from "conformal/valibot";
import * as v from "valibot";

v.object({
name: v.optional(vf.string()),
email: v.pipe(vf.string(), v.email()),
age: v.pipe(vf.number(), v.minValue(16)),
hobbies: vf.array(vf.string()),
birthDate: vf.date(),
acceptTerms: vf.boolean(),
profilePicture: vf.file(),
accountType: vf.picklist(["personal", "business"]),
website: v.optional(v.pipe(vf.string(), v.url())),
transactionAmount: vf.bigint(),
name: v.pipe(vf.coerceString(), v.string()),
email: v.pipe(vf.coerceString(), v.string(), v.email()),
age: v.pipe(vf.coerceNumber(), v.number(), v.minValue(16)),
hobbies: v.pipe(
vf.coerceArray(),
v.array(v.pipe(vf.coerceString(), v.string())),
),
birthDate: v.pipe(vf.coerceDate(), v.date()),
acceptTerms: v.pipe(vf.coerceBoolean(), v.boolean()),
profilePicture: v.pipe(vf.coerceFile(), v.file()),
transactionAmount: v.pipe(vf.coerceBigint(), v.bigint()),
});
```
42 changes: 42 additions & 0 deletions src/valibot/coerce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as v from "valibot";
import * as coerce from "../coerce.js";

export function coerceString(): v.SchemaWithPipe<
readonly [v.UnknownSchema, v.TransformAction<unknown, unknown>]
> {
return v.pipe(v.unknown(), v.transform(coerce.coerceString));
}

export function coerceNumber(): v.SchemaWithPipe<
readonly [v.UnknownSchema, v.TransformAction<unknown, unknown>]
> {
return v.pipe(v.unknown(), v.transform(coerce.coerceNumber));
}

export function coerceBigint() {
return v.pipe(v.unknown(), v.transform(coerce.coerceBigint));
}

export function coerceBoolean(): v.SchemaWithPipe<
readonly [v.UnknownSchema, v.TransformAction<unknown, unknown>]
> {
return v.pipe(v.unknown(), v.transform(coerce.coerceBoolean));
}

export function coerceDate(): v.SchemaWithPipe<
readonly [v.UnknownSchema, v.TransformAction<unknown, unknown>]
> {
return v.pipe(v.unknown(), v.transform(coerce.coerceDate));
}

export function coerceFile(): v.SchemaWithPipe<
readonly [v.UnknownSchema, v.TransformAction<unknown, unknown>]
> {
return v.pipe(v.unknown(), v.transform(coerce.coerceFile));
}

export function coerceArray(): v.SchemaWithPipe<
readonly [v.UnknownSchema, v.TransformAction<unknown, unknown[]>]
> {
return v.pipe(v.unknown(), v.transform(coerce.coerceArray));
}
17 changes: 8 additions & 9 deletions src/valibot/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
export {
string,
number,
boolean,
date,
bigint,
picklist,
file,
array,
} from "./schemas.js";
coerceArray,
coerceBigint,
coerceBoolean,
coerceDate,
coerceFile,
coerceNumber,
coerceString,
} from "./coerce.js";
64 changes: 0 additions & 64 deletions src/valibot/schemas.ts

This file was deleted.

6 changes: 3 additions & 3 deletions test/coerce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ describe("coerce functions", () => {
expect(coerceNumber("-10")).toBe(-10);
});

it("should return NaN for invalid number strings", () => {
expect(coerceNumber("abc")).toBeNaN();
expect(coerceNumber("12.34.56")).toBeNaN();
it("should return original string for invalid number strings", () => {
expect(coerceNumber("abc")).toBe("abc");
expect(coerceNumber("12.34.56")).toBe("12.34.56");
});
});

Expand Down
Loading