1# Schema: Migration from v3
2
3This document maps v3 Schema APIs to their v4 equivalents. Simple renames and argument changes are covered in the summary table below. More complex migrations have dedicated sections with code examples.
4
5## Migration types
6
7- **auto** — mechanical find-and-replace, safe to auto-apply
8- **semi-auto** — follows a clear pattern but needs structural changes
9- **manual** — requires case-by-case decisions, flag for human review
10- **removed** — no v4 equivalent
11
12## Summary table
13
14| v3 API | v4 API | Migration type |
15| ----------------------------------------------- | ----------------------------------------------------------------------------- | ----------------- |
16| `asSchema(schema)` | `revealCodec(schema)` | rename |
17| `encodedSchema(schema)` | `toEncoded(schema)` | rename |
18| `typeSchema(schema)` | `toType(schema)` | rename |
19| `compose(schemaB)` | `decodeTo(schemaB)` | rename |
20| `annotations(ann)` | `annotate(ann)` | rename |
21| `decodingFallback` annotation | `catchDecoding(...)` | rename |
22| `parseJson()` | `fromJsonString(Unknown)` | rename |
23| `parseJson(schema)` | `fromJsonString(schema)` | rename |
24| `pattern(regex)` | `check(isPattern(regex))` | rename |
25| `nonEmptyString` | `isNonEmpty` | rename |
26| `BigIntFromSelf` | `BigInt` | rename |
27| `SymbolFromSelf` | `Symbol` | rename |
28| `URLFromSelf` | `URL` | rename |
29| `RedactedFromSelf` | `Redacted` | rename |
30| `Redacted` | `RedactedFromValue` | rename |
31| `EitherFromSelf` | `Result` | rename |
32| `DateFromNumber` | `DateFromMillis` | rename |
33| `Date` | `DateFromString` | restructure |
34| `decodeUnknown` | `decodeUnknownEffect` | rename |
35| `decode` | `decodeEffect` | rename |
36| `decodeUnknownEither` | `decodeUnknownExit` | rename |
37| `decodeEither` | `decodeExit` | rename |
38| `encodeUnknown` | `encodeUnknownEffect` | rename |
39| `encode` | `encodeEffect` | rename |
40| `encodeUnknownEither` | `encodeUnknownExit` | rename |
41| `encodeEither` | `encodeExit` | rename |
42| `asserts(schema)(input)` | `asserts(schema, input)` | semi-auto |
43| `Literal(null)` | `Null` | restructure |
44| `Literal("a", "b")` | `Literals(["a", "b"])` | variadic-to-array |
45| `pickLiteral("a", "b")` | `Literals(...).pick(["a", "b"])` | restructure |
46| `Union(A, B)` | `Union([A, B])` | variadic-to-array |
47| `Tuple(A, B)` | `Tuple([A, B])` | variadic-to-array |
48| `TemplateLiteral(A, B)` | `TemplateLiteral([A, B])` | variadic-to-array |
49| `TemplateLiteralParser(A, B)` | `TemplateLiteralParser(schema.parts)` | restructure |
50| `Record({ key, value })` | `Record(key, value)` | restructure |
51| `filter(predicate)` | `check(makeFilter(predicate))` | restructure |
52| `filter(refinement)` | `refine(refinement)` | restructure |
53| `UUID` | `String.check(isUUID())` | restructure |
54| `ULID` | `String.check(isULID())` | restructure |
55| `pick("a")` | `mapFields(Struct.pick(["a"]))` | restructure |
56| `omit("a")` | `mapFields(Struct.omit(["a"]))` | restructure |
57| `partial` | `mapFields(Struct.map(Schema.optional))` | restructure |
58| `partialWith({ exact: true })` | `mapFields(Struct.map(Schema.optionalKey))` | restructure |
59| `required(schema)` | `schema.mapFields(Struct.map(Schema.requiredKey))` | restructure |
60| `extend(structB)` | `mapFields(Struct.assign(fieldsB))` or `fieldsAssign(fieldsB)` | restructure |
61| `transform(from, to, { decode, encode })` | `from.pipe(decodeTo(to, SchemaTransformation.transform({ decode, encode })))` | restructure |
62| `transformOrFail(from, to, { decode, encode })` | `from.pipe(decodeTo(to, { decode: SchemaGetter.transformEffect(...), ... }))` | restructure |
63| `transformLiteral(from, to)` | `Literal(from).transform(to)` | restructure |
64| `transformLiterals([0,"a"], [1,"b"])` | `Literals([0, 1]).transform(["a", "b"])` | restructure |
65| `attachPropertySignature("k", "v")` | `mapFields(f => ({...f, k: tagDefaultOmit("v")}))` | restructure |
66| `validate*` | removed (use `decode*` + `toType`) | removed |
67| `keyof` | — | removed |
68| `NonEmptyArrayEnsure` | — | removed |
69| `withDefaults` | — | removed |
70| `Data(schema)` | — | removed |
71| `optionalWith(schema, opts)` | varies by options (see [optionalWith](#optionalwith)) | manual |
72| `optionalToOptional` | see [optional field transformations](#optional-field-transformations) | manual |
73| `optionalToRequired` | see [optional field transformations](#optional-field-transformations) | manual |
74| `requiredToOptional` | see [optional field transformations](#optional-field-transformations) | manual |
75| `filterEffect` | see [filterEffect](#filtereffect) | manual |
76| `fromKey` | see [rename](#rename) | manual |
77| `rename({ a: "c" })` | see [rename](#rename) | manual |
78| `format(schema)` | see [format](#format) | manual |
79| `ParseResult.ArrayFormatter.formatError(error)` | see [ParseResult formatters](#parseresult-formatters) | manual |
80| `declare` | see [declare](#declare) | manual |
81
82## Additional rename notes
83
84### `*FromSelf` renames
85
86The following `*FromSelf` schemas have been renamed to drop the suffix:
87
88`DateFromSelf` → `Date`, `DurationFromSelf` → `Duration`, `ChunkFromSelf` → `Chunk`, `ReadonlyMapFromSelf` → `ReadonlyMap`, `ReadonlySetFromSelf` → `ReadonlySet`, `HashMapFromSelf` → `HashMap`, `HashSetFromSelf` → `HashSet`, `BigDecimalFromSelf` → `BigDecimal`, `CauseFromSelf` → `Cause`, `ExitFromSelf` → `Exit`, `OptionFromSelf` → `Option`, `RegExpFromSelf` → `RegExp`
89
90### `Date` encoded contract
91
92**Migration: restructure**
93
94In v3, `Schema.Date` decoded an ISO date string to a `Date` and rejected invalid dates. In v4, `Schema.Date` is the renamed `Schema.DateFromSelf`, so it expects a valid `Date` as its encoded value. Existing code can still type-check after upgrading while no longer accepting the same input.
95
96v3
97
98```ts
99import { Schema } from "effect"
100
101const DateFromIsoString = Schema.Date
102```
103
104v4
105
106```ts
107import { Schema } from "effect"
108
109const DateFromIsoString = Schema.DateFromString
110```
111
112`Schema.DateFromString` preserves the string-to-`Date` transformation and rejects strings that produce invalid dates.
113
114### Filter renames
115
116All filters have been renamed with an `is` prefix and now use `check(...)` or `pipe(Schema.check(...))`:
117
118`greaterThan` → `isGreaterThan`, `greaterThanOrEqualTo` → `isGreaterThanOrEqualTo`, `lessThan` → `isLessThan`, `lessThanOrEqualTo` → `isLessThanOrEqualTo`, `between` → `isBetween`, `int` → `isInt`, `multipleOf` → `isMultipleOf`, `finite` → `isFinite`, `minLength` → `isMinLength`, `maxLength` → `isMaxLength`, `length` → `isBetweenLength`
119
120Note: `positive`, `negative`, `nonNegative`, `nonPositive` have been removed in v4.
121
122### Utility renames
123
124`equivalence` → `toEquivalence`, `arbitrary` → [`Arbitrary.schema`](#migrating-from-the-fast-check-bridge-to-native-arbitrary) from `effect/Arbitrary`, `pretty` → `toFormatter`, `standardSchemaV1` → `toStandardSchemaV1`
125
126## Detailed migrations
127
128### Redacted
129
130**Migration: rename with behavior distinction**
131
132In v3, `Schema.Redacted(value)` decoded the raw encoded value and wrapped the decoded value in `Redacted`.
133
134In v4, that behavior is named `Schema.RedactedFromValue(value)`.
135
136v3
137
138```ts
139import { Schema } from "effect"
140
141const schema = Schema.Redacted(Schema.String)
142const decode = Schema.decodeSync(schema)
143
144decode("secret")
145```
146
147v4
148
149```ts
150import { Redacted, Schema } from "effect"
151
152const schema = Schema.RedactedFromValue(Schema.String)
153const decode = Schema.decodeSync(schema)
154
155const redacted = decode("secret")
156console.log(Redacted.value(redacted))
157// secret
158```
159
160`Schema.Redacted(value)` in v4 is the replacement for v3 `Schema.RedactedFromSelf(value)`: it expects the input to already be a `Redacted` value, so both `Type` and `Encoded` are `Redacted<...>`.
161
162### asserts signature
163
164**Migration: semi-auto**
165
166`Schema.asserts` now asserts an input directly instead of returning an assertion function.
167
168v3
169
170```ts
171import { Schema } from "effect"
172
173const assertString = Schema.asserts(Schema.String)
174assertString(input)
175```
176
177v4
178
179```ts
180import { Schema } from "effect"
181
182Schema.asserts(Schema.String, input)
183```
184
185### validate* removal
186
187**Migration: removed**
188
189The `validate`, `validateEither`, `validatePromise`, `validateSync`, and `validateOption` APIs have been removed. Use `Schema.decode*` + `Schema.toType` instead.
190
191```ts
192import { Schema } from "effect"
193
194// v3: Schema.validateSync(Schema.String)(input)
195// v4:
196const validateSync = Schema.decodeSync(Schema.toType(Schema.String))
197```
198
199### Data removal
200
201**Migration: removed**
202
203`Schema.Data` has no v4 equivalent. Remove it. `Equal.equals` performs deep structural comparison on objects by default in v4, so `Schema.Data` is unnecessary.
204
205### pickLiterals
206
207**Migration: auto**
208
209v3
210
211```ts
212import { Schema } from "effect"
213
214const schema = Schema.Literal("a", "b", "c").pipe(Schema.pickLiteral("a", "b"))
215```
216
217v4
218
219```ts
220import { Schema } from "effect"
221
222const schema = Schema.Literals(["a", "b", "c"]).pick(["a", "b"])
223```
224
225### TemplateLiteralParser
226
227**Migration: semi-auto**
228
229v3
230
231```ts
232import { Schema } from "effect"
233
234const schema = Schema.TemplateLiteral(Schema.String, ".", Schema.String)
235const parser = Schema.TemplateLiteralParser(Schema.String, ".", Schema.String)
236```
237
238v4
239
240```ts
241import { Schema } from "effect"
242
243const schema = Schema.TemplateLiteral([Schema.String, ".", Schema.String])
244// use the `parts` property instead of repeating the template parts
245const parser = Schema.TemplateLiteralParser(schema.parts)
246```
247
248Behavior note: `TemplateLiteral` and `TemplateLiteralParser` match parts semantically. Checks on string, number, and bigint schema parts are applied while matching each segment, so refined parts can reject strings that would match the broader primitive shape.
249
250### format
251
252**Migration: manual**
253
254**New imports:** `SchemaRepresentation`
255
256v3
257
258```ts
259import { Schema } from "effect"
260
261console.log(Schema.format(Schema.String))
262// string
263```
264
265v4
266
267```ts
268import { Schema, SchemaRepresentation } from "effect"
269
270const doc = SchemaRepresentation.toRepresentation(Schema.String.ast)
271const multi = SchemaRepresentation.toMultiDocument(doc)
272const codeDoc = SchemaRepresentation.toCodeDocument(multi)
273console.log(codeDoc.codes[0].Type)
274// string
275```
276
277### ParseResult formatters
278
279**Migration: manual**
280
281**New imports:** `SchemaIssue`
282
283In v4, schema parsing fails with `Schema.SchemaError`, which contains a nested `SchemaIssue` in its `issue` field.
284
285Use `SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues` for the v3 `ParseResult.ArrayFormatter.formatError(error)` equivalent.
286
287v3
288
289```ts
290import { Either, ParseResult, Schema } from "effect"
291
292const Person = Schema.Struct({
293 name: Schema.String,
294 age: Schema.Number
295})
296
297const decode = Schema.decodeUnknownEither(Person)
298
299const result = decode({})
300if (Either.isLeft(result)) {
301 console.error("Decoding failed:")
302 console.error(ParseResult.ArrayFormatter.formatErrorSync(result.left))
303}
304/*
305Decoding failed:
306[ { _tag: 'Missing', path: [ 'name' ], message: 'is missing' } ]
307*/
308```
309
310v4
311
312```ts
313import { Schema, SchemaIssue } from "effect"
314
315const Person = Schema.Struct({
316 name: Schema.String,
317 age: Schema.Number
318})
319
320const decode = Schema.decodeUnknownSync(Person)
321
322try {
323 decode({})
324} catch (error) {
325 if (error instanceof Error) {
326 console.error("Decoding failed:")
327 if (SchemaIssue.isIssue(error.cause)) {
328 console.error(SchemaIssue.makeFormatterStandardSchemaV1()(error.cause).issues)
329 }
330 }
331}
332/*
333Decoding failed:
334[ { path: [ 'name' ], message: 'Missing key' } ]
335*/
336```
337
338### Record
339
340**Migration: auto**
341
342v3
343
344```ts
345import { Schema } from "effect"
346
347const schema = Schema.Record({ key: Schema.String, value: Schema.Number })
348```
349
350v4
351
352```ts
353import { Schema } from "effect"
354
355const schema = Schema.Record(Schema.String, Schema.Number)
356```
357
358Behavior note: dynamic record key schemas select matching own properties before the value schema is applied. Refined key schemas such as `Schema.String.check(...)`, `Schema.Int`, or checked template literals ignore properties that do not match the key schema; they do not validate the value at those ignored keys. For transformed key schemas, selection is based on encoded property names before selected keys are decoded.
359
360### pick / omit
361
362**Migration: semi-auto**
363
364**New imports:** `Struct`
365
366v3
367
368```ts
369import { Schema } from "effect"
370
371const picked = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(Schema.pick("a"))
372const omitted = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(Schema.omit("b"))
373```
374
375v4
376
377```ts
378import { Schema, Struct } from "effect"
379
380const picked = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.pick(["a"]))
381const omitted = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.omit(["b"]))
382```
383
384### partial / partialWith
385
386**Migration: semi-auto**
387
388**New imports:** `Struct`
389
390- `Schema.partial` → `mapFields(Struct.map(Schema.optional))` (allows `undefined`)
391- `Schema.partialWith({ exact: true })` → `mapFields(Struct.map(Schema.optionalKey))` (exact)
392
393```ts
394import { Schema, Struct } from "effect"
395
396const struct = Schema.Struct({ a: Schema.String, b: Schema.Number })
397
398// v3: struct.pipe(Schema.partial)
399const withUndefined = struct.mapFields(Struct.map(Schema.optional))
400
401// v3: struct.pipe(Schema.partialWith({ exact: true }))
402const exact = struct.mapFields(Struct.map(Schema.optionalKey))
403```
404
405You can also make a subset of fields partial:
406
407```ts
408import { Schema, Struct } from "effect"
409
410const schema = Schema.Struct({ a: Schema.String, b: Schema.Number })
411 .mapFields(Struct.mapPick(["a"], Schema.optional))
412```
413
414### required
415
416**Migration: semi-auto**
417
418**New imports:** `Struct`
419
420- `Schema.requiredKey`: makes `optionalKey` fields required; makes `optional` fields required as `T | undefined`
421- `Schema.required`: makes `optional` fields required (removes `undefined`)
422
423```ts
424import { Schema, Struct } from "effect"
425
426const original = Schema.Struct({
427 a: Schema.optionalKey(Schema.String),
428 b: Schema.optionalKey(Schema.Number)
429})
430
431// v3: Schema.required(original)
432const schema = original.mapFields(Struct.map(Schema.requiredKey))
433// { readonly a: string; readonly b: number; }
434```
435
436### optional field transformations
437
438**Migration: manual**
439
440**New imports:** `SchemaGetter`
441
442`optionalToOptional`, `optionalToRequired`, and `requiredToOptional` are all replaced by `Schema.decodeTo` + `SchemaGetter.transformOptional`.
443
444The pattern: start with the encoded optionality (`optionalKey` or required), pipe to `decodeTo` with the decoded optionality, and provide `transformOptional` functions for decode/encode.
445
446**Example** (v3 `optionalToRequired`: setting `null` as default for missing field)
447
448v3
449
450```ts
451import { Option, Schema } from "effect"
452
453const schema = Schema.Struct({
454 a: Schema.optionalToRequired(Schema.String, Schema.NullOr(Schema.String), {
455 decode: Option.getOrElse(() => null),
456 encode: Option.liftPredicate((value) => value !== null)
457 })
458})
459```
460
461v4
462
463```ts
464import { Option, Schema, SchemaGetter } from "effect"
465
466const schema = Schema.Struct({
467 a: Schema.optionalKey(Schema.String).pipe(
468 Schema.decodeTo(Schema.NullOr(Schema.String), {
469 decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)),
470 encode: SchemaGetter.transformOptional(Option.filter((value) => value !== null))
471 })
472 )
473})
474```
475
476**Example** (v3 `requiredToOptional`: empty string as missing value)
477
478v3
479
480```ts
481import { Option, Schema } from "effect"
482
483const schema = Schema.Struct({
484 a: Schema.requiredToOptional(Schema.String, Schema.String, {
485 decode: Option.liftPredicate((s) => s !== ""),
486 encode: Option.getOrElse(() => "")
487 })
488})
489```
490
491v4
492
493```ts
494import { Option, Schema, SchemaGetter } from "effect"
495
496const schema = Schema.Struct({
497 a: Schema.String.pipe(
498 Schema.decodeTo(Schema.optionalKey(Schema.String), {
499 decode: SchemaGetter.transformOptional(Option.filter((value) => value !== "")),
500 encode: SchemaGetter.transformOptional(Option.orElseSome(() => ""))
501 })
502 )
503})
504```
505
506### optionalWith
507
508**Migration: manual**
509
510**New imports:** `SchemaGetter`, `Predicate` (for nullable variants)
511
512#### Decision tree
513
514| v3 options | v4 pattern |
515| ------------------------------------------ | ----------------------------------------------------------------------- |
516| `{ exact: true }` | `optionalKey(schema)` |
517| `{ default }` | `schema.pipe(withDecodingDefaultType(...))` |
518| `{ exact: true, default }` | `schema.pipe(withDecodingDefaultTypeKey(...))` |
519| `{ nullable: true }` | `optional(NullOr(schema))` + `decodeTo` + filter null |
520| `{ nullable: true, exact: true }` | `optionalKey(NullOr(schema))` + `decodeTo` + filter null |
521| `{ nullable: true, default }` | `optional(NullOr(schema))` + `decodeTo` + filter null + `orElseSome` |
522| `{ nullable: true, exact: true, default }` | `optionalKey(NullOr(schema))` + `decodeTo` + filter null + `orElseSome` |
523
524Key rules:
525
526- `exact: true` → use `optionalKey` instead of `optional`
527- `nullable: true` → wrap inner schema in `NullOr` and filter nulls via `Option.filter(Predicate.isNotNull)`
528- `default` → use `withDecodingDefaultType` (or `withDecodingDefaultTypeKey` with `exact: true`)
529
530#### Example: `{ exact: true }` (simplest case)
531
532v3
533
534```ts
535import { Schema } from "effect"
536
537const schema = Schema.Struct({
538 a: Schema.optionalWith(Schema.NumberFromString, { exact: true })
539})
540```
541
542v4
543
544```ts
545import { Schema } from "effect"
546
547const schema = Schema.Struct({
548 a: Schema.optionalKey(Schema.NumberFromString)
549})
550```
551
552#### Example: `{ default }`
553
554v3
555
556```ts
557import { Schema } from "effect"
558
559const schema = Schema.Struct({
560 a: Schema.optionalWith(Schema.String, { default: () => "" })
561})
562```
563
564v4
565
566```ts
567import { Effect, Schema } from "effect"
568
569const schema = Schema.Struct({
570 a: Schema.String.pipe(Schema.withDecodingDefaultType(Effect.succeed("")))
571})
572```
573
574#### Example: `{ exact: true, default }`
575
576v3
577
578```ts
579import { Schema } from "effect"
580
581const schema = Schema.Struct({
582 a: Schema.optionalWith(Schema.String, { exact: true, default: () => "" })
583})
584```
585
586v4
587
588```ts
589import { Effect, Schema } from "effect"
590
591const schema = Schema.Struct({
592 a: Schema.String.pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed("")))
593})
594```
595
596#### Example: `{ nullable: true, exact: true, default }` (most complex case)
597
598v3
599
600```ts
601import { Schema } from "effect"
602
603const schema = Schema.Struct({
604 a: Schema.optionalWith(Schema.NumberFromString, { nullable: true, default: () => -1, exact: true })
605})
606```
607
608v4
609
610```ts
611import { Option, Predicate, Schema, SchemaGetter } from "effect"
612
613const schema = Schema.Struct({
614 a: Schema.optionalKey(Schema.NullOr(Schema.NumberFromString)).pipe(
615 Schema.decodeTo(Schema.Number, {
616 decode: SchemaGetter.transformOptional((o) =>
617 o.pipe(Option.filter(Predicate.isNotNull), Option.orElseSome(() => -1))
618 ),
619 encode: SchemaGetter.required()
620 })
621 )
622})
623```
624
625### pluck
626
627**Migration: manual**
628
629**New imports:** `SchemaGetter`, `Struct`
630
631v3
632
633```ts
634import { Schema } from "effect"
635
636const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(Schema.pluck("a"))
637```
638
639v4
640
641```ts
642import { Schema, SchemaGetter, Struct } from "effect"
643
644function pluck<P extends PropertyKey>(key: P) {
645 return <S extends Schema.Top>(
646 schema: Schema.Struct<{ [K in P]: S }>
647 ): Schema.decodeTo<Schema.toType<S>, Schema.Struct<{ [K in P]: S }>> => {
648 return schema.mapFields(Struct.pick([key])).pipe(
649 Schema.decodeTo(Schema.toType(schema.fields[key]), {
650 decode: SchemaGetter.transform((whole: any) => whole[key]),
651 encode: SchemaGetter.transform((value) => ({ [key]: value } as any))
652 })
653 )
654 }
655}
656
657const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(pluck("a"))
658```
659
660### extend
661
662**Migration: semi-auto**
663
664**New imports:** `Struct` (Struct case), `Tuple` (Union case)
665
666#### Struct extends Struct
667
668v3
669
670```ts
671import { Schema } from "effect"
672
673const schema = Schema.Struct({
674 a: Schema.String,
675 b: Schema.Number
676}).pipe(Schema.extend(Schema.Struct({ c: Schema.Number })))
677```
678
679v4
680
681```ts
682import { Schema, Struct } from "effect"
683
684const schema = Schema.Struct({
685 a: Schema.String,
686 b: Schema.Number
687}).mapFields(Struct.assign({ c: Schema.Number }))
688
689// or more succinctly
690const schema2 = Schema.Struct({
691 a: Schema.String,
692 b: Schema.Number
693}).pipe(Schema.fieldsAssign({ c: Schema.Number }))
694```
695
696#### Union extends Struct
697
698v3
699
700```ts
701import { Schema } from "effect"
702
703const schema = Schema.Union(
704 Schema.Struct({ a: Schema.String }),
705 Schema.Struct({ b: Schema.Number })
706).pipe(Schema.extend(Schema.Struct({ c: Schema.Boolean })))
707```
708
709v4
710
711```ts
712import { Schema, Tuple } from "effect"
713
714const schema = Schema.Union([
715 Schema.Struct({ a: Schema.String }),
716 Schema.Struct({ b: Schema.Number })
717]).mapMembers(Tuple.map(Schema.fieldsAssign({ c: Schema.Number })))
718```
719
720### filter
721
722**Migration: semi-auto**
723
724v3
725
726```ts
727import { Schema } from "effect"
728
729// inline filter
730const a = Schema.String.pipe(Schema.filter((s) => s.length > 0))
731
732// refinement
733const b = Schema.Option(Schema.String).pipe(Schema.filter(Option.isSome))
734```
735
736v4
737
738```ts
739import { Option, Schema } from "effect"
740
741// inline filter
742const a = Schema.String.check(Schema.makeFilter((s) => s.length > 0))
743
744// refinement
745const b = Schema.Option(Schema.String).pipe(Schema.refine(Option.isSome))
746```
747
748In v4, a `makeFilter` predicate can return any of the shapes described by `Schema.FilterOutput`:
749
750- `undefined` / `true` — success
751- `false` — generic failure
752- `string` — failure with that message
753- `SchemaIssue.Issue` — a fully-formed issue
754- `{ path, issue }` — failure at a nested path (`issue` is a `string` or `SchemaIssue.Issue`)
755- `ReadonlyArray<Schema.FilterIssue>` — several failures reported together (empty array = success, single element is unwrapped, otherwise grouped into an `Issue.Composite`)
756
757**Example** (Failure at a nested path)
758
759```ts
760import { Schema } from "effect"
761
762const schema = Schema.Struct({ password: Schema.String, confirmPassword: Schema.String }).check(
763 Schema.makeFilter((o) =>
764 o.password === o.confirmPassword
765 ? undefined
766 : { path: ["password"], issue: "password and confirmPassword must match" }
767 )
768)
769
770console.log(String(Schema.decodeUnknownExit(schema)({ password: "123456", confirmPassword: "1234567" })))
771// Failure(Cause([Fail(SchemaError: password and confirmPassword must match
772// at ["password"])]))
773```
774
775**Example** (Reporting multiple failures at once)
776
777```ts
778import { Schema } from "effect"
779
780const schema = Schema.Struct({ a: Schema.Finite, b: Schema.Finite, c: Schema.Finite }).check(
781 Schema.makeFilter((o) => {
782 const issues: Array<Schema.FilterIssue> = []
783 if (o.a > 0) {
784 if (o.b <= 0) issues.push({ path: ["b"], issue: "b must be greater than 0" })
785 if (o.c <= 0) issues.push({ path: ["c"], issue: "c must be greater than 0" })
786 }
787 return issues
788 })
789)
790
791console.log(String(Schema.decodeUnknownExit(schema)({ a: 1, b: 0, c: 0 })))
792// Failure(Cause([Fail(SchemaError: b must be greater than 0
793// at ["b"]
794// c must be greater than 0
795// at ["c"])]))
796```
797
798### filterEffect
799
800**Migration: manual**
801
802**New imports:** `SchemaGetter`, `Result`
803
804v3
805
806```ts
807import { Effect, Schema } from "effect"
808
809async function validateUsername(username: string) {
810 return Promise.resolve(username === "gcanti")
811}
812
813const ValidUsername = Schema.String.pipe(
814 Schema.filterEffect((username) =>
815 Effect.promise(() => validateUsername(username).then((valid) => valid || "Invalid username"))
816 )
817)
818```
819
820v4
821
822```ts
823import { Effect, Result, Schema, SchemaGetter } from "effect"
824
825async function validateUsername(username: string) {
826 return Promise.resolve(username === "gcanti")
827}
828
829const ValidUsername = Schema.String.pipe(
830 Schema.decode({
831 decode: SchemaGetter.checkEffect((username) =>
832 Effect.promise(() => validateUsername(username).then((valid) => valid || "Invalid username"))
833 ),
834 encode: SchemaGetter.passthrough()
835 })
836)
837```
838
839### transform
840
841**Migration: semi-auto**
842
843**New imports:** `SchemaTransformation`
844
845v3
846
847```ts
848import { Schema } from "effect"
849
850const BooleanFromString = Schema.transform(Schema.Literal("on", "off"), Schema.Boolean, {
851 strict: true,
852 decode: (literal) => literal === "on",
853 encode: (bool) => (bool ? "on" : "off")
854})
855```
856
857v4
858
859```ts
860import { Schema, SchemaTransformation } from "effect"
861
862const BooleanFromString = Schema.Literals(["on", "off"]).pipe(
863 Schema.decodeTo(
864 Schema.Boolean,
865 SchemaTransformation.transform({
866 decode: (literal) => literal === "on",
867 encode: (bool) => (bool ? "on" : "off")
868 })
869 )
870)
871```
872
873### transformOrFail
874
875**Migration: semi-auto**
876
877**New imports:** `SchemaGetter`, `SchemaIssue`
878
879v3
880
881```ts
882import { ParseResult, Schema } from "effect"
883
884const NumberFromString = Schema.transformOrFail(Schema.String, Schema.Number, {
885 strict: true,
886 decode: (input, _, ast) => {
887 const parsed = parseFloat(input)
888 if (isNaN(parsed)) {
889 return ParseResult.fail(new ParseResult.Type(ast, input, "Failed to convert string to number"))
890 }
891 return ParseResult.succeed(parsed)
892 },
893 encode: (input) => ParseResult.succeed(input.toString())
894})
895```
896
897v4
898
899```ts
900import { Effect, Number, Schema, SchemaGetter, SchemaIssue } from "effect"
901
902const NumberFromString = Schema.String.pipe(
903 Schema.decodeTo(Schema.Number, {
904 decode: SchemaGetter.transformEffect((s) => {
905 const n = Number.parse(s)
906 if (n === undefined) {
907 return Effect.fail(new SchemaIssue.InvalidValue())
908 }
909 return Effect.succeed(n)
910 }),
911 encode: SchemaGetter.String()
912 })
913)
914```
915
916### transformLiteral / transformLiterals
917
918**Migration: auto**
919
920v3
921
922```ts
923import { Schema } from "effect"
924
925const a = Schema.transformLiteral(0, "a")
926const b = Schema.transformLiterals([0, "a"], [1, "b"], [2, "c"])
927```
928
929v4
930
931```ts
932import { Schema } from "effect"
933
934const a = Schema.Literal(0).transform("a")
935const b = Schema.Literals([0, 1, 2]).transform(["a", "b", "c"])
936```
937
938### attachPropertySignature
939
940**Migration: semi-auto**
941
942v3
943
944```ts
945import { Schema } from "effect"
946
947const Circle = Schema.Struct({ radius: Schema.Number })
948const Square = Schema.Struct({ sideLength: Schema.Number })
949
950const DiscriminatedShape = Schema.Union(
951 Circle.pipe(Schema.attachPropertySignature("kind", "circle")),
952 Square.pipe(Schema.attachPropertySignature("kind", "square"))
953)
954```
955
956v4
957
958```ts
959import { Schema } from "effect"
960
961const Circle = Schema.Struct({ radius: Schema.Number })
962const Square = Schema.Struct({ sideLength: Schema.Number })
963
964const DiscriminatedShape = Schema.Union([
965 Circle.mapFields((fields) => ({ ...fields, kind: Schema.tagDefaultOmit("circle") })),
966 Square.mapFields((fields) => ({ ...fields, kind: Schema.tagDefaultOmit("square") }))
967])
968```
969
970### decodingFallback
971
972**Migration: auto**
973
974v3
975
976```ts
977import { Effect, Schema } from "effect"
978
979const schema = Schema.String.annotations({
980 decodingFallback: () => Effect.succeed("a")
981})
982```
983
984v4
985
986```ts
987import { Effect, Schema } from "effect"
988
989const schema = Schema.String.pipe(Schema.catchDecoding(() => Effect.succeedSome("a")))
990```
991
992### rename
993
994**Migration: manual**
995
996**New imports:** `SchemaTransformation`
997
998v3
999
1000```ts
1001import { Schema } from "effect"
1002
1003const schema = Schema.Struct({
1004 a: Schema.String,
1005 b: Schema.Number
1006}).pipe(Schema.rename({ a: "c" }))
1007```
1008
1009v4
1010
1011```ts
1012import { Schema } from "effect"
1013
1014// experimental API
1015const schema = Schema.Struct({
1016 a: Schema.String,
1017 b: Schema.Number
1018}).pipe(Schema.encodeKeys({ a: "c" }))
1019```
1020
1021### Capitalize / Lowercase / Uppercase / Uncapitalize
1022
1023**Migration: semi-auto**
1024
1025**New imports:** `SchemaTransformation`
1026
1027v3
1028
1029```ts
1030import { Schema } from "effect"
1031
1032const schema = Schema.Capitalize
1033```
1034
1035v4
1036
1037```ts
1038import { Schema, SchemaTransformation } from "effect"
1039
1040const schema = Schema.String.pipe(
1041 Schema.decodeTo(Schema.String.check(Schema.isCapitalized()), SchemaTransformation.capitalize())
1042)
1043```
1044
1045### NonEmptyTrimmedString
1046
1047**Migration: semi-auto**
1048
1049v3
1050
1051```ts
1052import { Schema } from "effect"
1053
1054const schema = Schema.NonEmptyTrimmedString
1055```
1056
1057v4
1058
1059```ts
1060import { Schema } from "effect"
1061
1062const schema = Schema.Trimmed.check(Schema.isNonEmpty())
1063```
1064
1065### split
1066
1067**Migration: manual**
1068
1069**New imports:** `SchemaTransformation`
1070
1071v3
1072
1073```ts
1074import { Schema } from "effect"
1075
1076const schema = Schema.split(",")
1077```
1078
1079v4
1080
1081```ts
1082import { Schema, SchemaTransformation } from "effect"
1083
1084function split(separator: string) {
1085 return Schema.String.pipe(
1086 Schema.decodeTo(
1087 Schema.Array(Schema.String),
1088 SchemaTransformation.transform({
1089 decode: (s) => s.split(separator) as ReadonlyArray<string>,
1090 encode: (as) => as.join(separator)
1091 })
1092 )
1093 )
1094}
1095```
1096
1097## Migrating from the fast-check bridge to native Arbitrary
1098
1099This section covers migration from the fast-check bridge published in `effect@4.0.0-rc.109` to the native,
1100Schema-first module at `effect/Arbitrary`.
1101
1102The new module removes fast-check from the `effect` package. Applications may still install and use fast-check
1103directly, but Effect Schema generation and `@effect/vitest` property tests no longer depend on it.
1104
1105For the new API and its semantics, see [Arbitrary in Effect](../packages/effect/ARBITRARY.md).
1106
1107### Import changes
1108
1109The following APIs have been removed:
1110
1111- `effect/testing/FastCheck`;
1112- `Schema.toArbitrary`;
1113- `Schema.Arbitrary`;
1114- the legacy `Schema.Annotations.ToArbitrary` contract and declaration-level `toArbitrary` annotation;
1115- the legacy `arbitrary` filter annotation;
1116- raw fast-check arbitrary inputs and `fastCheck` options in `@effect/vitest`.
1117
1118Import the native module explicitly:
1119
1120```ts
1121import * as Arbitrary from "effect/Arbitrary"
1122```
1123
1124If other tests still use fast-check-specific APIs, add fast-check as a direct development dependency and import it
1125from `"fast-check"`. Do not import it through Effect.
1126
1127### Generating samples
1128
1129Previously, `Schema.toArbitrary` returned a factory that needed the fast-check module:
1130
1131```ts
1132import { Schema } from "effect"
1133import { FastCheck } from "effect/testing"
1134
1135const Person = Schema.Struct({
1136 name: Schema.String,
1137 age: Schema.Int
1138})
1139
1140const personArbitrary = Schema.toArbitrary(Person)(FastCheck)
1141const samples = FastCheck.sample(personArbitrary, { numRuns: 20, seed: 42 })
1142```
1143
1144Now derive and sample through the Effect-native module:
1145
1146```ts
1147import { Effect, Schema } from "effect"
1148import * as Arbitrary from "effect/Arbitrary"
1149
1150const Person = Schema.Struct({
1151 name: Schema.String,
1152 age: Schema.Int
1153})
1154
1155const personArbitrary = Arbitrary.schema(Person)
1156const samples = await Effect.runPromise(
1157 Arbitrary.sampleEffect(personArbitrary, { count: 20, seed: 42 })
1158)
1159```
1160
1161`Arbitrary.sampleEffect` returns an `Effect` because sampling is interruptible, uses Effect `Random` when no seed is
1162provided, and reports bounded generation exhaustion as a typed `SampleError`.
1163
1164The generated values still use the decoded Schema `Type`. The sequence and distribution are not compatible with
1165fast-check, even when the same numeric seed is used.
1166
1167### Checking properties
1168
1169Previously, fast-check owned both the property and the runner:
1170
1171```ts
1172import { Schema } from "effect"
1173import { FastCheck } from "effect/testing"
1174
1175const integer = Schema.toArbitrary(Schema.Int)(FastCheck)
1176
1177FastCheck.assert(
1178 FastCheck.property(integer, (value) => Number.isInteger(value)),
1179 { numRuns: 100, seed: 42 }
1180)
1181```
1182
1183Now `Arbitrary.checkEffect` runs a pure or Effectful property and returns a structured result:
1184
1185```ts
1186import { Effect, Schema } from "effect"
1187import * as Arbitrary from "effect/Arbitrary"
1188
1189const result = await Effect.runPromise(
1190 Arbitrary.checkEffect(
1191 Arbitrary.schema(Schema.Int),
1192 (value) => Number.isInteger(value),
1193 { runs: 100, seed: 42 }
1194 )
1195)
1196```
1197
1198Unlike `FastCheck.assert`, `Arbitrary.checkEffect` does not throw for an ordinary falsification. Handle `Passed`,
1199`Falsified`, `Exhausted`, and `ReplayMismatch` explicitly, or use `@effect/vitest`, which converts non-passing results
1200into test failures.
1201
1202Typed failures from Effectful properties are preserved in `Falsified.failure`. Defects and interruption continue
1203through the returned Effect.
1204
1205### Option mapping
1206
1207The most common options map as follows:
1208
1209| Previous fast-check option | Native option | Migration note |
1210| -------------------------- | -------------------- | ------------------------------------------------------------------ |
1211| `numRuns` | `count` or `runs` | Use `count` for `sampleEffect` and `runs` for `checkEffect`. |
1212| `seed` | `seed` | The type is compatible, but generated sequences are not. |
1213| `path` | `replay` | Existing fast-check paths cannot be converted. |
1214| `maxSkipsPerRun` | `maxDiscards` | Native uses one absolute discard budget, not a multiplier per run. |
1215| `examples` | No direct equivalent | Keep explicit regression cases as ordinary tests. |
1216| `endOnFailure` | `maxShrinks` | Use `maxShrinks: 0` to stop at the initial failure. |
1217| `interruptAfterTimeLimit` | Effect interruption | Apply an Effect or test timeout around the check. |
1218| `skipAllAfterTimeLimit` | No direct equivalent | Prefer explicit run and discard bounds. |
1219| `verbose` | No direct equivalent | Inspect `CheckResult` or use `@effect/vitest` failure output. |
1220
1221Review any less common fast-check runner option manually. The native API deliberately does not reproduce the complete
1222`fc.Parameters` surface.
1223
1224### Replay migration
1225
1226Fast-check replay used a seed plus a shrink `path`. Native replay uses one opaque token returned by a `Falsified`
1227result:
1228
1229```ts
1230const replayed = Arbitrary.checkEffect(arbitrary, property, {
1231 replay: previousFailure.replay
1232})
1233```
1234
1235There is no conversion from a fast-check seed and path to a native replay token. Re-run the property with the native
1236engine, then record the new token from its `Falsified` result.
1237
1238Replay tokens are intended for reproducing and diagnosing a current failure. Because the module is unstable, they are
1239not guaranteed to survive upgrades. Preserve important failing inputs as explicit regression tests.
1240
1241Array shrinking now tries deleting prefixes and interior blocks as well as suffixes. Products and objects retain child
1242shrink candidates when exploring other branches. Shrunk outputs and replay paths can therefore differ from earlier
1243native releases, including RC.115. Re-run affected properties to record new tokens.
1244For arrays of composed Arbitraries, use `Arbitrary.array(item, { maxLength: 50 })` instead of shrinking a generated
1245length through `flatMap` and `all`; deletions then preserve the remaining generated elements.
1246
1247### Migrating declaration annotations
1248
1249The old `toArbitrary` annotation directly constructed a fast-check arbitrary and exposed fast-check recursion and
1250constraint details:
1251
1252```ts
1253import { Schema } from "effect"
1254
1255class UserId {
1256 readonly value: number
1257 constructor(value: number) {
1258 this.value = value
1259 }
1260}
1261
1262const UserIdSchema = Schema.instanceOf(UserId, {
1263 toArbitrary: () => (fc) => fc.integer({ min: 1, max: 1_000_000 }).map((value) => new UserId(value))
1264})
1265```
1266
1267The native `toCodecArbitrary` annotation describes a generatable representation as a Schema `Link`:
1268
1269```ts
1270import { Schema, SchemaTransformation } from "effect"
1271
1272class UserId {
1273 readonly value: number
1274 constructor(value: number) {
1275 this.value = value
1276 }
1277}
1278
1279const UserIdSchema = Schema.instanceOf(UserId, {
1280 toCodecArbitrary: () =>
1281 Schema.link<UserId>()(
1282 Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000_000 })),
1283 SchemaTransformation.transform({
1284 decode: (value) => new UserId(value),
1285 encode: (id) => id.value
1286 })
1287 )
1288})
1289```
1290
1291Before adding `toCodecArbitrary`, check whether the declaration already has a useful `toCodecJson` or `toCodec`.
1292Native derivation falls back to those canonical codecs automatically. Add an arbitrary-specific Link only when the
1293canonical representation is opaque or generates valid values too rarely.
1294
1295The migration changes where generation logic lives:
1296
1297| Legacy contract | Native contract |
1298| ---------------------------------------------- | -------------------------------------------------------------- |
1299| Returns a `fast-check.Arbitrary` | Returns a `SchemaAST.Link` through `Schema.link`. |
1300| Receives generated arbitrary type parameters | Receives decoded Schema type parameters. |
1301| Receives fast-check constraints and recursion | Receives normalized constraints. |
1302| Manages terminal recursive branches explicitly | Leaves recursion analysis and budgets to the native compiler. |
1303| Uses arbitrary combinators | Uses Schema constructors, checks, and a Schema transformation. |
1304
1305The original declaration remains authoritative. Values decoded by the Link are checked against it. Failed decodes and
1306rejected values become bounded discards.
1307
1308#### Custom filter metadata
1309
1310The old `arbitrary` filter annotation has been replaced by `arbitraryConstraint`. Ordinary custom filters continue to
1311work as residual filters without generation metadata:
1312
1313```ts
1314import { Schema } from "effect"
1315
1316const Even = Schema.Int.check(
1317 Schema.makeFilter((value) => value % 2 === 0)
1318)
1319```
1320
1321Residual filtering is bounded, so a very selective or impossible predicate may produce `SampleError` or `Exhausted`.
1322
1323If the previous annotation supplied a recognized constructive constraint, move it to `arbitraryConstraint` and adapt
1324its shape. The predicate remains authoritative:
1325
1326```ts
1327import { Order, Schema } from "effect"
1328
1329const Positive = Schema.Number.check(
1330 Schema.makeFilter(
1331 (value) => value > 0,
1332 {
1333 arbitraryConstraint: {
1334 order: Order.Number,
1335 minimum: 0,
1336 exclusiveMinimum: true
1337 }
1338 }
1339 )
1340)
1341```
1342
1343The main constraint-shape changes are:
1344
1345| Previous field | Native field |
1346| ------------------------------------ | ------------------------------------------------------------------- |
1347| `ordered.order` | `order` |
1348| `ordered.minimum` / `maximum` | `minimum` / `maximum` |
1349| `ordered.exclusiveMinimum` / maximum | `exclusiveMinimum: true` / `exclusiveMaximum: true` |
1350| `integer: true` | `number: "integer"` |
1351| `noNaN` and `noInfinity` | `number: "finite"` when both restrictions apply |
1352| collection `minLength` / `maxLength` | `minLength`, `minSize`, or `minProperties` and its matching maximum |
1353| string pattern | `{ source, flags }` in `patterns` |
1354| `unique: true` | `uniqueBy: identity` |
1355| `candidate` | No direct equivalent |
1356
1357Choose the cardinality field that matches the Schema domain: `minLength` and `maxLength` for strings and arrays,
1358`minSize` and `maxSize` for sized collections, and `minProperties` and `maxProperties` for object properties.
1359
1360For an opaque declaration that needs a reusable statistically better source domain, express that source as a Schema
1361Link with `toCodecArbitrary`.
1362
1363### Migrating `@effect/vitest`
1364
1365Property inputs may be Schemas, native Arbitraries, or mixtures of both.
1366
1367Schema-only properties need only an option rename:
1368
1369```ts
1370// Before
1371it.prop(
1372 "commutative",
1373 [Schema.Int, Schema.Int],
1374 ([a, b]) => a + b === b + a,
1375 { fastCheck: { numRuns: 200, seed: 42 } }
1376)
1377
1378// After
1379it.prop(
1380 "commutative",
1381 [Schema.Int, Schema.Int],
1382 ([a, b]) => a + b === b + a,
1383 { arbitrary: { runs: 200, seed: 42 } }
1384)
1385```
1386
1387Raw or mixed fast-check inputs are no longer accepted:
1388
1389```ts
1390// No longer supported
1391it.prop("raw arbitrary", [fc.integer()], ([value]) => Number.isInteger(value))
1392it.prop("mixed", [Schema.String, fc.integer()], ([text, value]) => true)
1393```
1394
1395Replace those inputs with Schemas when they describe a domain supported by Schema, or compose a native Arbitrary:
1396
1397```ts
1398import * as Arbitrary from "effect/Arbitrary"
1399
1400const integer = Arbitrary.schema(Schema.Int)
1401
1402it.prop("native arbitrary", [integer], ([value]) => Number.isInteger(value))
1403it.prop("mixed", [Schema.String, integer], ([text, value]) => typeof text === "string" && Number.isInteger(value))
1404```
1405
1406If a test genuinely needs a fast-check-specific arbitrary or runner feature, use fast-check directly with Vitest
1407rather than passing it through `@effect/vitest`.
1408
1409`it.prop`, `it.effect.prop`, and `it.live.prop` all accept native check options under `arbitrary`.
1410
1411### Behavioral differences to review
1412
1413Migration is not only an import rename. Review the following differences:
1414
1415- native generation and shrinking have different distributions and may find different shrunk inputs;
1416- native checking returns structured results instead of using fast-check's assertion exceptions;
1417- generation that cannot find enough valid samples is bounded and reports `SampleError` or `Exhausted`;
1418- pure and Effectful properties share one interruptible runner;
1419- recursive and mutually recursive Schemas are analyzed as a graph and must have a finite generation path;
1420- replay tokens, seeds, and shrink paths are not compatible with fast-check;
1421- generated values are the decoded Schema `Type`;
1422- properties must not mutate generated values.
1423
1424### Migration checklist
1425
14261. Replace `effect/testing/FastCheck` imports. Use the native Arbitrary module for Schema generation and import
1427 `"fast-check"` directly only where it is still independently required.
14282. Replace `Schema.toArbitrary(schema)(FastCheck)` with `Arbitrary.schema(schema)`.
14293. Replace `FastCheck.sample` with `Arbitrary.sampleEffect` and run the returned Effect.
14304. Replace `FastCheck.check` or `FastCheck.assert` for Schema-derived inputs with `Arbitrary.checkEffect`, then handle its
1431 structured result.
14325. Rename `@effect/vitest` options from `fastCheck` to `arbitrary` and convert `numRuns` to `runs`.
14336. Replace raw fast-check inputs in `@effect/vitest` with Schemas or native Arbitraries.
14347. Migrate declaration-level `toArbitrary` callbacks to the `toCodecArbitrary` Link-returning contract and replace old
1435 filter-level `arbitrary` annotations with `arbitraryConstraint`.
14368. Re-run properties with the native engine and record new replay tokens or explicit regression examples.
14379. Review discard limits for selective custom filters.