Repository: edmundhung/conform
Pull request #925: fix: re-support Valibot `config`/`fallback` methods for piped primitive value schemas
Author: login: nanto
Created: 2025-05-02T18:30:19Z | Merged: 2025-05-03T18:30:55Z

Valibot `config`/`fallback` methods for piped primitive value schemas had worked until conform-to-valibot v1.14.1 but stop working since v1.14.2 (when chimame/conform-to-valibot#57 was merged). This pull request make them work again.

```javascript
// Example of config method for piped primitive value schema
const schema = v.object({
  key: v.config(
    v.pipe(v.number(), v.minValue(0), v.maxValue(10)),
    { message: 'Out of range' },
  ),
});
```

This pull request doesn't treat `config`/`fallback` methods for kinds of object schemas, which have not worked since before conform-to-valibot v1.14.1. (Maybe we have to modify Valibot internal data structure to support those cases.)

```javascript
// Example of fallback method for object schema
const schema = v.fallback(
  v.object({ key: v.number() }),
  { key: 0 },
);
```

Fixes #924


Linked issue #924: [@conform-to/valibot] `config`/`fallback` methods don't work with piped primitive value schemas
### Describe the bug and the expected behavior

When we call `parseWithValibot` function with a Valibot schema that contains `config`/`fallback` methods for piped primitive value schemas, the effect of `config`/`fallback` methods disappears.

### Conform version

@conform-to/valibot v1.0.1

### Steps to Reproduce the Bug or Issue

Run the following test:

```javascript
import { describe, expect, test } from 'vitest';
import { config, empty, object, pipe, string } from 'valibot';
import { parseWithValibot } from '@conform-to/valibot';

describe('config', () => {
  test('should fail with message', () => {
    const schema = object({
      key: config(pipe(string(), empty()), { message: 'wrong value' }),
    });
    const input = new URLSearchParams({ key: 'value' });
    const output = parseWithValibot(input, { schema });
    expect(output).toMatchObject({
      status: 'error',
      error: { key: ['wrong value'] },
    });
  });
});
```

Actual output value: `{ status: 'error', error: { key: ['Invalid length: Expected 0 but received 5'] }, ... }`

Expected output value: `{ status: 'error', error: { key: ['wrong value'] }, ... }`


### What browsers are you seeing the problem on?

_No response_

### Screenshots or Videos

_No response_

### Additional context

This issue is a regression by chimame/conform-to-valibot#57 (since conform-to-valibot v1.14.2).

Diff:
diff --git a/.changeset/strange-weeks-grab.md b/.changeset/strange-weeks-grab.md
new file mode 100644
index 000000000..36b1c41a1
--- /dev/null
+++ b/.changeset/strange-weeks-grab.md
@@ -0,0 +1,5 @@
+---
+'@conform-to/valibot': patch
+---
+
+Fix support `config` and `fallback` again
diff --git a/packages/conform-valibot/coercion.ts b/packages/conform-valibot/coercion.ts
index 21b02373a..b559df7d4 100644
--- a/packages/conform-valibot/coercion.ts
+++ b/packages/conform-valibot/coercion.ts
@@ -290,6 +290,18 @@ function enableTypeCoercion<T extends GenericSchema | GenericSchemaAsync>(
 			type.pipe[0],
 			options,
 		);
+
+		if (transformAction) {
+			// `expects` is required to generate error messages for `TupleSchema`, so it is passed to `UnkonwSchema` for coercion.
+			const unknown = { ...valibotUnknown(), expects: type.expects };
+			// Reuse `type` to preserve behavior added by Valibot `config` and/or `fallback` methods.
+			const schema = type.async
+				? pipeAsync(unknown, transformAction, type)
+				: pipe(unknown, transformAction, type);
+
+			return { transformAction, schema };
+		}
+
 		const schema = type.async
 			? pipeAsync(coercedSchema, ...type.pipe.slice(1))
 			: // @ts-expect-error `coercedSchema` must be sync here but TypeScript can't infer that.
diff --git a/packages/conform-valibot/tests/coercion/method/config.test.ts b/packages/conform-valibot/tests/coercion/method/config.test.ts
new file mode 100644
index 000000000..7476615e1
--- /dev/null
+++ b/packages/conform-valibot/tests/coercion/method/config.test.ts
@@ -0,0 +1,18 @@
+import { config, empty, object, pipe, string } from 'valibot';
+import { describe, expect, test } from 'vitest';
+import { parseWithValibot } from '../../../parse';
+import { createFormData } from '../../helpers/FormData';
+
+describe('config', () => {
+	test('should fail with message', () => {
+		const schema = object({
+			key: config(pipe(string(), empty()), { message: 'wrong value' }),
+		});
+		const input = createFormData('key', 'value');
+		const output = parseWithValibot(input, { schema });
+		expect(output).toMatchObject({
+			status: 'error',
+			error: { key: ['wrong value'] },
+		});
+	});
+});
diff --git a/packages/conform-valibot/tests/coercion/method/fallback.test.ts b/packages/conform-valibot/tests/coercion/method/fallback.test.ts
new file mode 100644
index 000000000..d9ab4f9a6
--- /dev/null
+++ b/packages/conform-valibot/tests/coercion/method/fallback.test.ts
@@ -0,0 +1,18 @@
+import { empty, fallback, object, pipe, string } from 'valibot';
+import { describe, expect, test } from 'vitest';
+import { parseWithValibot } from '../../../parse';
+import { createFormData } from '../../helpers/FormData';
+
+describe('fallback', () => {
+	test('should pass with fallback', () => {
+		const schema = object({
+			key: fallback(pipe(string(), empty()), 'fallback value'),
+		});
+		const input = createFormData('key', 'value');
+		const output = parseWithValibot(input, { schema });
+		expect(output).toMatchObject({
+			status: 'success',
+			value: { key: 'fallback value' },
+		});
+	});
+});


Reviews:
user: login: chimame
body: Thank you for the pull request!
It's very helpful to have your help in creating code that solves the problem.

I also thought about the code to solve the problem, and I think it could be a little simpler. Can you check it?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-03T02:34:36Z
user: login: nanto
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T17:32:19Z
user: login: nanto
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T17:45:55Z
user: login: chimame
body: Thanks for the fix.
Let's merge this!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-03T18:30:30Z
user: login: chimame
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-03T18:44:24Z

Review comments:
diff_hunk: @@ -285,19 +315,6 @@ function enableTypeCoercion<T extends GenericSchema | GenericSchemaAsync>(
 	transformAction: TransformAction<unknown, unknown> | undefined;
 	schema: GenericSchema | GenericSchemaAsync;
 } {
-	if ('pipe' in type) {
path: packages/conform-valibot/coercion.ts
user: login: chimame
body: Here's the code that addresses the problem you pointed out in the original code. How about making the `if ('pipe' in type)` true case into a function? That way you don't need an `if` in the `switch`.

```diff
if ('pipe' in type) {
  const { transformAction, schema: coercedSchema } = enableTypeCoercion(
    type.pipe[0],
    options,
  );

+  if (transformAction) {
+    const unknown = { ...valibotUnknown(), expects: type.expects };
+    const schema = type.async
+      ? pipeAsync(unknown, transformAction, type)
+      : pipe(unknown, transformAction, type);
+
+    return { transformAction, schema };
+  }

  const schema = type.async
    ? pipeAsync(coercedSchema, ...type.pipe.slice(1))
    : // @ts-expect-error `coercedSchema` must be sync here but TypeScript can't infer that.
      pipe(coercedSchema, ...type.pipe.slice(1));

  return { transformAction, schema };
}
```

created_at: 2025-05-03T02:29:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,18 @@
+import { config, empty, object, pipe, string } from 'valibot';
path: packages/conform-valibot/tests/coercion/schema/config.test.ts
user: login: chimame
body: Thanks for creating the test.
`fallback` and `config` are actions, not schemas, so could you put them in `tests/coercion/action/config.test.ts`?
created_at: 2025-05-03T02:31:29Z
author_association: COLLABORATOR
diff_hunk: @@ -285,19 +315,6 @@ function enableTypeCoercion<T extends GenericSchema | GenericSchemaAsync>(
 	transformAction: TransformAction<unknown, unknown> | undefined;
 	schema: GenericSchema | GenericSchemaAsync;
 } {
-	if ('pipe' in type) {
path: packages/conform-valibot/coercion.ts
user: login: nanto
body: I see. Simple is better!

Creating schemas that may not be used can slow down execution time when pipes are nested many times ([I make a benchmark](https://gist.github.com/nanto/873606402bbdae027663a3d823a0bfae)). However, the case pipes are nested many times is not practical, I think.
created_at: 2025-05-03T17:32:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+import { config, empty, object, pipe, string } from 'valibot';
path: packages/conform-valibot/tests/coercion/schema/config.test.ts
user: login: nanto
body: `fallback` and `config` are categorized as methods so I moved the test files to `tests/coercion/method/`.

By the way, since `test/coercion/method/` directory is created, it might be better to move some test cases in [`test/coercion/schema/wrap.test.ts`](https://github.com/edmundhung/conform/blob/33de60262d5cf73ed12761eed9d018d890d2e753/packages/conform-valibot/tests/coercion/schema/wrap.test.ts#L50-L86) and [`test/coercion/schema/wrapAsync.test.ts`](https://github.com/edmundhung/conform/blob/33de60262d5cf73ed12761eed9d018d890d2e753/packages/conform-valibot/tests/coercion/schema/wrapAsync.test.ts#L55-L108) into `test/coercion/method/pipe{,Async}.test.ts`.
created_at: 2025-05-03T17:45:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+import { config, empty, object, pipe, string } from 'valibot';
path: packages/conform-valibot/tests/coercion/schema/config.test.ts
user: login: chimame
body: You're right.
I'll move on to that in the future.
created_at: 2025-05-03T18:44:24Z
author_association: COLLABORATOR

PR comments:
user: login: pkg-pr-new[bot]
created_at: 2025-05-02T18:32:01Z
body: 

<details><summary><b>More templates</b></summary><p>

- [@conform-example/chakra-ui](https://pkg.pr.new/template/59f98637-8257-4747-9f34-c3b93848cb7d)
- [@conform-example/headless-ui](https://pkg.pr.new/template/041647f9-6099-4647-80ef-13aa95729c64)
- [@conform-example/material-ui](https://pkg.pr.new/template/f0277814-1a2c-44cf-abe7-0658f52c3b9c)
- [@conform-example/nextjs](https://pkg.pr.new/template/38f5c0d0-aef5-4ace-8975-302d446a7fe3)
- [@conform-example/radix-ui](https://pkg.pr.new/template/1a082ca8-69ef-4b8a-8c5f-19ff33aa80b4)
- [@conform-example/react-router](https://pkg.pr.new/template/8ce09ff6-fe81-4cd5-bf4a-36cc915b930a)
- [@conform-example/remix](https://pkg.pr.new/template/38518a1a-1f10-4208-b389-1c946248eee4)
- [@conform-example/shadcn-ui](https://pkg.pr.new/template/eb9f692f-5816-4c64-a88e-84d2ff79417b)

</p></details>
      
    


<details><summary><b>@conform-to/dom</b></summary><p>

```
npm i https://pkg.pr.new/@conform-to/dom@925
```

</p></details>
      
    

<details><summary><b>@conform-to/react</b></summary><p>

```
npm i https://pkg.pr.new/@conform-to/react@925
```

</p></details>
      
    

<details><summary><b>@conform-to/validitystate</b></summary><p>

```
npm i https://pkg.pr.new/@conform-to/validitystate@925
```

</p></details>
      
    

<details><summary><b>@conform-to/valibot</b></summary><p>

```
npm i https://pkg.pr.new/@conform-to/valibot@925
```

</p></details>
      
    

<details><summary><b>@conform-to/yup</b></summary><p>

```
npm i https://pkg.pr.new/@conform-to/yup@925
```

</p></details>
      
    

<details><summary><b>@conform-to/zod</b></summary><p>

```
npm i https://pkg.pr.new/@conform-to/zod@925
```

</p></details>
      
    

_commit: <a href="https://github.com/edmundhung/conform/runs/41591126607"><code>507804b</code></a>_

author_association: NONE
user: login: nanto
created_at: 2025-05-02T18:38:42Z
body: This pull request reverts change of coerced schema structure (<https://github.com/chimame/conform-to-valibot/pull/57#issuecomment-2646435844>) since `config`/`fallback` information is lost by that change.
author_association: CONTRIBUTOR
user: login: changeset-bot[bot]
created_at: 2025-05-03T17:13:38Z
body: ###  🦋  Changeset detected

Latest commit: 507804bbc069b314a53dc17190dface8159652f2

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                | Type  |
| ------------------- | ----- |
| @conform-to/valibot | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/nanto/conform/new/valibot-config-fallback-for-primitive?filename=.changeset/lovely-bottles-hammer.md&value=---%0A%22%40conform-to%2Fvalibot%22%3A%20patch%0A---%0A%0Afix%3A%20re-support%20Valibot%20%60config%60%2F%60fallback%60%20methods%20for%20piped%20primitive%20value%20schemas%0A)


author_association: NONE
user: login: nanto
created_at: 2025-05-03T17:51:59Z
body: Thank you for your review! I modified codes and changed test file paths.
author_association: CONTRIBUTOR