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
1 change: 1 addition & 0 deletions packages/data-schema/.changes/minor.schema-transform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `Schema.transform()` for mapping validated schema outputs to new values and output types.
24 changes: 24 additions & 0 deletions packages/data-schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,30 @@ let Profile = object({
})
```

## Output transforms with `.transform()`

Map a validated value into the shape your app wants. The transformer runs after the schema validates and changes the parsed output type.

```ts
import { object, parse, string } from '@remix-run/data-schema'

let Event = object({
id: string(),
createdAt: string()
.refine((value) => !Number.isNaN(Date.parse(value)), 'Expected valid date')
.transform((value) => new Date(value)),
})

let event = parse(Event, {
id: 'evt_1',
createdAt: '2026-04-25T00:00:00.000Z',
})

event.createdAt // Date
```

Use `.refine()` for checks that reject values without changing them. Use `.transform()` for safe, synchronous mappings; thrown errors are propagated instead of converted into validation issues.

## Validation pipelines with `.pipe()`

Compose reusable `Check` objects for common constraints.
Expand Down
34 changes: 34 additions & 0 deletions packages/data-schema/src/lib/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,18 @@ describe('modifiers', () => {
assertFailure(result)
assert.equal(result.issues[0].message, 'Must be positive')
})

it('supports transform functions', () => {
let schema = number().transform((n) => n.toString())
expectType<Equal<InferOutput<typeof schema>, string>>()

let ok = schema['~standard'].validate(42)
let bad = schema['~standard'].validate('not-a-number')

assertSuccess(ok)
assert.equal(ok.value, '42')
assertFailure(bad)
})
})

describe('tuple', () => {
Expand Down Expand Up @@ -796,6 +808,28 @@ describe('modifiers (additional)', () => {
assertFailure(schema['~standard'].validate('bcd'))
assertFailure(schema['~standard'].validate('ab'))
})

it('chains transform then refine', () => {
let schema = number()
.transform((n) => n.toString())
.refine((s) => s.length === 2, 'Must be two digits')

assertSuccess(schema['~standard'].validate(42))
assertFailure(schema['~standard'].validate(7))
})

it('chains refine then transform', () => {
let schema = number()
.refine((n) => n > 0, 'Must be positive')
.transform((n) => n.toString())

let ok = schema['~standard'].validate(42)
let bad = schema['~standard'].validate(-1)

assertSuccess(ok)
assert.equal(ok.value, '42')
assertFailure(bad)
})
})

describe('abortEarly', () => {
Expand Down
21 changes: 21 additions & 0 deletions packages/data-schema/src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ export type Schema<input, output = input> = SyncStandardSchema<input, output> &
* @returns A new schema with the refinement applied
*/
refine: (predicate: (value: output) => boolean, message?: string) => Schema<input, output>
/**
* Transform the output of this schema with a function.
*
* The transform function runs after the underlying schema has validated and produced an `output` value.
* The returned schema has a different output type.
*
* @param transformer A function that transforms the validated output
* @returns A new schema with the transformation applied
*/
transform: <newOutput>(transformer: (value: output) => newOutput) => Schema<input, newOutput>
/**
* Internal validator used to validate nested values while preserving `path`/`options`.
*/
Expand Down Expand Up @@ -214,6 +224,17 @@ export function createSchema<input, output>(
return result
})
},
transform<newOutput>(transformer: (value: output) => newOutput): Schema<input, newOutput> {
return createSchema<input, newOutput>(function validate(value, context) {
let result = schema['~run'](value, context)

if (result.issues) {
return result
}

return { value: transformer(result.value) }
})
},
}

return schema
Expand Down
Loading