Skip to main content

Overview

Schema has undergone significant restructuring in v4. This guide maps v3 Schema APIs to their v4 equivalents, organized by migration complexity.

Migration Types

  • rename — simple find-and-replace, safe to auto-apply
  • variadic-to-array — convert variadic arguments to array syntax
  • restructure — follows a clear pattern but needs structural changes
  • manual — requires case-by-case decisions
  • removed — no v4 equivalent

Quick Reference Table

v3 APIv4 APIType
asSchema(schema)revealCodec(schema)rename
encodedSchema(schema)toEncoded(schema)rename
typeSchema(schema)toType(schema)rename
compose(schemaB)decodeTo(schemaB)rename
annotations(ann)annotate(ann)rename
decodingFallback annotationcatchDecoding(...)rename
parseJson()UnknownFromJsonStringrename
parseJson(schema)fromJsonString(schema)rename
pattern(regex)check(isPattern(regex))rename
nonEmptyStringisNonEmptyrename
BigIntFromSelfBigIntrename
SymbolFromSelfSymbolrename
URLFromSelfURLrename
decodeUnknowndecodeUnknownEffectrename
decodedecodeEffectrename
Literal(null)Nullrestructure
Literal("a", "b")Literals(["a", "b"])variadic-to-array
Union(A, B)Union([A, B])variadic-to-array
Tuple(A, B)Tuple([A, B])variadic-to-array
Record({ key, value })Record(key, value)restructure
filter(predicate)check(makeFilter(predicate))restructure
UUIDString.check(isUUID())restructure
pick("a")mapFields(Struct.pick(["a"]))restructure
omit("a")mapFields(Struct.omit(["a"]))restructure
partialmapFields(Struct.map(Schema.optional))restructure
extend(structB)mapFields(Struct.assign(fieldsB))restructure
validate*removed (use decode* + toType)removed

Simple Renames

Core API Renames

v3
import { Schema } from "effect"

const encoded = Schema.encodedSchema(mySchema)
const type = Schema.typeSchema(mySchema)
const revealed = Schema.asSchema(mySchema)
const composed = mySchema.pipe(Schema.compose(otherSchema))
const annotated = mySchema.pipe(Schema.annotations({ title: "My Schema" }))
v4
import { Schema } from "effect"

const encoded = Schema.toEncoded(mySchema)
const type = Schema.toType(mySchema)
const revealed = Schema.revealCodec(mySchema)
const composed = mySchema.pipe(Schema.decodeTo(otherSchema))
const annotated = mySchema.pipe(Schema.annotate({ title: "My Schema" }))

Decode/Encode Renames

All decode and encode functions have been renamed with more explicit names: v3
import { Schema } from "effect"

const result1 = Schema.decodeUnknown(schema)(input)
const result2 = Schema.decode(schema)(input)
const result3 = Schema.decodeUnknownEither(schema)(input)
const result4 = Schema.decodeEither(schema)(input)
v4
import { Schema } from "effect"

const result1 = Schema.decodeUnknownEffect(schema)(input)
const result2 = Schema.decodeEffect(schema)(input)
const result3 = Schema.decodeUnknownExit(schema)(input)
const result4 = Schema.decodeExit(schema)(input)
Note the suffix change: EitherExit to align with Effect’s terminology.

*FromSelf Suffix Removal

All *FromSelf schemas have been renamed to drop the suffix: v3
import { Schema } from "effect"

const schemas = [
  Schema.BigIntFromSelf,
  Schema.SymbolFromSelf,
  Schema.URLFromSelf,
  Schema.DateFromSelf,
  Schema.OptionFromSelf(Schema.String),
  Schema.EitherFromSelf({ left: Schema.String, right: Schema.Number })
]
v4
import { Schema } from "effect"

const schemas = [
  Schema.BigInt,
  Schema.Symbol,
  Schema.URL,
  Schema.Date,
  Schema.Option(Schema.String),
  Schema.Result({ left: Schema.String, right: Schema.Number })
]
Also note: Either has been renamed to Result to align with v4 terminology.

Variadic to Array

Many constructors that accepted variadic arguments now require arrays:

Literals

v3
import { Schema } from "effect"

const schema = Schema.Literal("a", "b", "c")
const picked = Schema.Literal("a", "b", "c").pipe(Schema.pickLiteral("a", "b"))
v4
import { Schema } from "effect"

const schema = Schema.Literals(["a", "b", "c"])
const picked = Schema.Literals(["a", "b", "c"]).pick(["a", "b"])
Literal(null) should be replaced with the built-in Null schema.

Union and Tuple

v3
import { Schema } from "effect"

const union = Schema.Union(
  Schema.String,
  Schema.Number,
  Schema.Boolean
)

const tuple = Schema.Tuple(
  Schema.String,
  Schema.Number
)
v4
import { Schema } from "effect"

const union = Schema.Union([
  Schema.String,
  Schema.Number,
  Schema.Boolean
])

const tuple = Schema.Tuple([
  Schema.String,
  Schema.Number
])

TemplateLiteral

v3
import { Schema } from "effect"

const schema = Schema.TemplateLiteral(Schema.String, ".", Schema.String)
const parser = Schema.TemplateLiteralParser(Schema.String, ".", Schema.String)
v4
import { Schema } from "effect"

const schema = Schema.TemplateLiteral([Schema.String, ".", Schema.String])
// Use the `parts` property instead of repeating the template parts
const parser = Schema.TemplateLiteralParser(schema.parts)

Restructured APIs

Record

v3
import { Schema } from "effect"

const schema = Schema.Record({ key: Schema.String, value: Schema.Number })
v4
import { Schema } from "effect"

const schema = Schema.Record(Schema.String, Schema.Number)

pick / omit

Requires importing Struct from effect.
v3
import { Schema } from "effect"

const picked = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(
  Schema.pick("a")
)

const omitted = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(
  Schema.omit("b")
)
v4
import { Schema, Struct } from "effect"

const picked = Schema.Struct({ a: Schema.String, b: Schema.Number })
  .mapFields(Struct.pick(["a"]))

const omitted = Schema.Struct({ a: Schema.String, b: Schema.Number })
  .mapFields(Struct.omit(["b"]))

partial / required

v3
import { Schema } from "effect"

const struct = Schema.Struct({ a: Schema.String, b: Schema.Number })

const partial = struct.pipe(Schema.partial)
const exact = struct.pipe(Schema.partialWith({ exact: true }))
const required = partial.pipe(Schema.required)
v4
import { Schema, Struct } from "effect"

const struct = Schema.Struct({ a: Schema.String, b: Schema.Number })

// Allows undefined
const partial = struct.mapFields(Struct.map(Schema.optional))

// Exact optional (no undefined)
const exact = struct.mapFields(Struct.map(Schema.optionalKey))

// Make all fields required
const required = partial.mapFields(Struct.map(Schema.requiredKey))

// Make subset of fields partial
const mixedPartial = struct.mapFields(
  Struct.mapPick(["a"], Schema.optional)
)

extend

v3
import { Schema } from "effect"

const extended = Schema.Struct({
  a: Schema.String,
  b: Schema.Number
}).pipe(
  Schema.extend(Schema.Struct({ c: Schema.Boolean }))
)
v4
import { Schema, Struct } from "effect"

// Option 1: mapFields + Struct.assign
const extended = Schema.Struct({
  a: Schema.String,
  b: Schema.Number
}).mapFields(Struct.assign({ c: Schema.Boolean }))

// Option 2: fieldsAssign (more succinct)
const extended2 = Schema.Struct({
  a: Schema.String,
  b: Schema.Number
}).pipe(Schema.fieldsAssign({ c: Schema.Boolean }))

filter

v3
import { Schema } from "effect"

// Inline filter
const nonEmpty = Schema.String.pipe(
  Schema.filter((s) => s.length > 0)
)

// Refinement
const some = Schema.Option(Schema.String).pipe(
  Schema.filter(Option.isSome)
)
v4
import { Option, Schema } from "effect"

// Inline filter
const nonEmpty = Schema.String.check(
  Schema.makeFilter((s) => s.length > 0)
)

// Refinement
const some = Schema.Option(Schema.String).pipe(
  Schema.refine(Option.isSome)
)

Filter Predicates

All filter predicates have been renamed with is prefix and now use check(...): v3
import { Schema } from "effect"

const schemas = [
  Schema.String.pipe(Schema.pattern(/^\d+$/)),
  Schema.String.pipe(Schema.nonEmptyString),
  Schema.String.pipe(Schema.UUID),
  Schema.String.pipe(Schema.ULID),
  Schema.Number.pipe(Schema.greaterThan(0)),
  Schema.Number.pipe(Schema.int),
  Schema.Number.pipe(Schema.positive)
]
v4
import { Schema } from "effect"

const schemas = [
  Schema.String.check(Schema.isPattern(/^\d+$/)),
  Schema.String.check(Schema.isNonEmpty()),
  Schema.String.check(Schema.isUUID()),
  Schema.String.check(Schema.isULID()),
  Schema.Number.check(Schema.isGreaterThan(0)),
  Schema.Number.check(Schema.isInt())
  // Note: positive/negative/nonNegative/nonPositive removed in v4
]

transform / transformOrFail

v3
import { Schema } from "effect"

const BoolFromString = Schema.transform(
  Schema.Literal("on", "off"),
  Schema.Boolean,
  {
    strict: true,
    decode: (literal) => literal === "on",
    encode: (bool) => (bool ? "on" : "off")
  }
)
v4
import { Schema, SchemaTransformation } from "effect"

const BoolFromString = Schema.Literals(["on", "off"]).pipe(
  Schema.decodeTo(
    Schema.Boolean,
    SchemaTransformation.transform({
      decode: (literal) => literal === "on",
      encode: (bool) => (bool ? "on" : "off")
    })
  )
)
Requires importing SchemaTransformation from effect.
transformOrFail follows a similar pattern: v3
import { ParseResult, Schema } from "effect"

const NumberFromString = Schema.transformOrFail(
  Schema.String,
  Schema.Number,
  {
    strict: true,
    decode: (input, _, ast) => {
      const parsed = parseFloat(input)
      if (isNaN(parsed)) {
        return ParseResult.fail(
          new ParseResult.Type(ast, input, "Failed to convert")
        )
      }
      return ParseResult.succeed(parsed)
    },
    encode: (input) => ParseResult.succeed(input.toString())
  }
)
v4
import { Effect, Number, Schema, SchemaGetter, SchemaIssue } from "effect"

const NumberFromString = Schema.String.pipe(
  Schema.decodeTo(Schema.Number, {
    decode: SchemaGetter.transformOrFail((s) => {
      const n = Number.parse(s)
      if (n === undefined) {
        return Effect.fail(new SchemaIssue.InvalidValue(Option.some(s)))
      }
      return Effect.succeed(n)
    }),
    encode: SchemaGetter.String()
  })
)

decodingFallback

v3
import { Effect, Schema } from "effect"

const schema = Schema.String.annotations({
  decodingFallback: () => Effect.succeed("a")
})
v4
import { Effect, Schema } from "effect"

const schema = Schema.String.pipe(
  Schema.catchDecoding(() => Effect.succeedSome("a"))
)

Manual Migrations

optionalWith

The optionalWith API has been split into multiple explicit patterns based on the options used:
v3 optionsv4 pattern
{ exact: true }optionalKey(schema)
{ default }optional(schema) + decodeTo + withDefault(...)
{ exact: true, default }optionalKey(schema) + decodeTo + withDefault(...)
{ nullable: true }optional(NullOr(schema)) + filter null
{ nullable: true, default }optional(NullOr(schema)) + filter null + orElseSome
Example: { exact: true } (simplest case) v3
import { Schema } from "effect"

const schema = Schema.Struct({
  a: Schema.optionalWith(Schema.NumberFromString, { exact: true })
})
v4
import { Schema } from "effect"

const schema = Schema.Struct({
  a: Schema.optionalKey(Schema.NumberFromString)
})
Example: { nullable: true, exact: true, default } (most complex case) v3
import { Schema } from "effect"

const schema = Schema.Struct({
  a: Schema.optionalWith(Schema.NumberFromString, {
    nullable: true,
    default: () => -1,
    exact: true
  })
})
v4
import { Option, Predicate, Schema, SchemaGetter } from "effect"

const schema = Schema.Struct({
  a: Schema.optionalKey(Schema.NullOr(Schema.NumberFromString)).pipe(
    Schema.decodeTo(Schema.toType(Schema.NumberFromString), {
      decode: SchemaGetter.transformOptional((o) =>
        o.pipe(
          Option.filter(Predicate.isNotNull),
          Option.orElseSome(() => -1)
        )
      ),
      encode: SchemaGetter.required()
    })
  )
})

Optional Field Transformations

optionalToOptional, optionalToRequired, and requiredToOptional are replaced by Schema.decodeTo + SchemaGetter.transformOptional. Example: optionalToRequired (setting default for missing field) v3
import { Option, Schema } from "effect"

const schema = Schema.Struct({
  a: Schema.optionalToRequired(Schema.String, Schema.NullOr(Schema.String), {
    decode: Option.getOrElse(() => null),
    encode: Option.liftPredicate((value) => value !== null)
  })
})
v4
import { Option, Schema, SchemaGetter } from "effect"

const schema = Schema.Struct({
  a: Schema.optionalKey(Schema.String).pipe(
    Schema.decodeTo(Schema.NullOr(Schema.String), {
      decode: SchemaGetter.transformOptional(
        Option.orElseSome(() => null)
      ),
      encode: SchemaGetter.transformOptional(
        Option.filter((value) => value !== null)
      )
    })
  )
})

filterEffect

v3
import { Effect, Schema } from "effect"

async function validateUsername(username: string) {
  return Promise.resolve(username === "gcanti")
}

const ValidUsername = Schema.String.pipe(
  Schema.filterEffect((username) =>
    Effect.promise(() =>
      validateUsername(username).then((valid) => valid || "Invalid username")
    )
  )
)
v4
import { Effect, Schema, SchemaGetter } from "effect"

async function validateUsername(username: string) {
  return Promise.resolve(username === "gcanti")
}

const ValidUsername = Schema.String.pipe(
  Schema.decode({
    decode: SchemaGetter.checkEffect((username) =>
      Effect.promise(() =>
        validateUsername(username).then((valid) => valid || "Invalid username")
      )
    ),
    encode: SchemaGetter.passthrough()
  })
)

rename (experimental)

v3
import { Schema } from "effect"

const schema = Schema.Struct({
  a: Schema.String,
  b: Schema.Number
}).pipe(Schema.rename({ a: "c" }))
v4
import { Schema } from "effect"

// experimental API
const schema = Schema.Struct({
  a: Schema.String,
  b: Schema.Number
}).pipe(Schema.encodeKeys({ a: "c" }))

Removed APIs

validate* APIs

The validate, validateEither, validatePromise, validateSync, and validateOption APIs have been removed. Use Schema.decode* + Schema.toType instead. v3
import { Schema } from "effect"

const validate = Schema.validateSync(Schema.String)
const result = validate(input)
v4
import { Schema } from "effect"

const validate = Schema.decodeSync(Schema.toType(Schema.String))
const result = validate(input)

Other Removed APIs

  • keyof — removed without direct replacement
  • ArrayEnsure — removed
  • NonEmptyArrayEnsure — removed
  • withDefaults — removed
  • fromKey — removed
  • forkAll — removed
  • forkWithErrorHandler — removed

String Transformation Helpers

Capitalize / Lowercase / Uppercase

v3
import { Schema } from "effect"

const schema = Schema.Capitalize
v4
import { Schema, SchemaTransformation } from "effect"

const schema = Schema.String.pipe(
  Schema.decodeTo(
    Schema.String.check(Schema.isCapitalized()),
    SchemaTransformation.capitalize()
  )
)

NonEmptyTrimmedString

v3
import { Schema } from "effect"

const schema = Schema.NonEmptyTrimmedString
v4
import { Schema } from "effect"

const schema = Schema.Trimmed.check(Schema.isNonEmpty())

Migration Checklist

  • Rename Union(A, B)Union([A, B]) (variadic to array)
  • Rename Tuple(A, B)Tuple([A, B]) (variadic to array)
  • Rename Literal("a", "b")Literals(["a", "b"])
  • Rename Record({ key, value })Record(key, value)
  • Rename all *FromSelf schemas (drop suffix)
  • Rename decodeUnknowndecodeUnknownEffect
  • Rename composedecodeTo
  • Update pick / omit to use mapFields + Struct
  • Update partial / required to use mapFields + Struct.map
  • Update extend to use fieldsAssign or mapFields + Struct.assign
  • Update filter to use check(makeFilter(...)) or refine(...)
  • Update filter predicates to use is* prefix and check(...)
  • Update transform to use decodeTo + SchemaTransformation.transform
  • Update optionalWith based on decision tree
  • Replace validate* with decode* + toType
  • Replace decodingFallback with catchDecoding

Build docs developers (and LLMs) love