Documentation Index
Fetch the complete documentation index at: https://effect-ts-effect-smol.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Schema v4 is a complete redesign of the Schema module with a focus on explicitness, composability, and tree-shaking. While the core concepts of decoding and encoding remain the same, the API surface has changed significantly.
Design Philosophy
Schema v4 adopts three core principles:
- Lightweight by default — Only import the features you need, keeping your bundle small
- Familiar API — Naming conventions and patterns are consistent with popular validation libraries
- Explicit — You choose which features to use. Nothing is included implicitly
Key Changes
Elementary Schemas
Built-in schemas for primitives remain largely the same:
import { Schema } from "effect"
// Primitives (unchanged)
Schema.String
Schema.Number
Schema.BigInt
Schema.Boolean
Schema.Symbol
Schema.Undefined
Schema.Null
Validation Rules with .check()
Validation rules are now applied with .check(...) instead of separate schema constructors:
import { Schema } from "effect"
const MinLength5 = Schema.String.pipe(Schema.minLength(5))
const Email = Schema.String.pipe(Schema.pattern(/^.+@.+$/))
String Validation Rules
import { Schema } from "effect"
Schema.String.check(Schema.isMaxLength(5))
Schema.String.check(Schema.isMinLength(5))
Schema.String.check(Schema.isLengthBetween(5, 10))
Schema.String.check(Schema.isPattern(/^[a-z]+$/))
Schema.String.check(Schema.isStartsWith("aaa"))
Schema.String.check(Schema.isEndsWith("zzz"))
Schema.String.check(Schema.isIncludes("---"))
Schema.String.check(Schema.isUppercased())
Schema.String.check(Schema.isLowercased())
Number Validation Rules
import { Schema } from "effect"
Schema.Number.check(Schema.isBetween({ minimum: 5, maximum: 10 }))
Schema.Number.check(Schema.isGreaterThan(5))
Schema.Number.check(Schema.isGreaterThanOrEqualTo(5))
Schema.Number.check(Schema.isLessThan(5))
Schema.Number.check(Schema.isLessThanOrEqualTo(5))
Schema.Number.check(Schema.isMultipleOf(5))
Schema.Number.check(Schema.isInt())
Schema.Number.check(Schema.isInt32())
Struct Schemas
Struct schemas use Schema.Struct with field definitions:
import { Schema } from "effect"
const User = Schema.Struct({
name: Schema.String,
email: Schema.String,
age: Schema.Number
})
Optional Fields
Optional fields are defined with Schema.optionalKey or Schema.optional:
import { Schema } from "effect"
const User = Schema.Struct({
name: Schema.String,
// Exact optional (key may be absent)
email: Schema.optionalKey(Schema.String),
// Optional with undefined (key may be absent or undefined)
phone: Schema.optional(Schema.String)
})
// With "exactOptionalPropertyTypes": true
type User = {
readonly name: string
readonly email?: string
readonly phone?: string | undefined
}
Mutable Fields
Fields are readonly by default. Use Schema.mutableKey for mutable properties:
import { Schema } from "effect"
const User = Schema.Struct({
name: Schema.String,
count: Schema.mutableKey(Schema.Number)
})
type User = {
readonly name: string
count: number
}
Decoding Defaults
Provide default values during decoding with Schema.withDecodingDefault:
import { Schema } from "effect"
const Config = Schema.Struct({
port: Schema.Number.pipe(Schema.withDecodingDefault(() => 8080))
})
Schema.decodeUnknownSync(Config)({}) // { port: 8080 }
Schema.decodeUnknownSync(Config)({ port: undefined }) // { port: 8080 }
Schema.decodeUnknownSync(Config)({ port: 3000 }) // { port: 3000 }
Tagged Structs
Tagged structs include a _tag field for discriminated unions:
import { Schema } from "effect"
const Success = Schema.TaggedStruct("Success", {
value: Schema.String
})
const Failure = Schema.TaggedStruct("Failure", {
error: Schema.String
})
const Result = Schema.Union([Success, Failure])
Tagged Unions
Define tagged unions with a single call:
import { Schema } from "effect"
const Result = Schema.TaggedUnion({
Success: { value: Schema.String },
Failure: { error: Schema.String }
})
type Result =
| { readonly _tag: "Success"; readonly value: string }
| { readonly _tag: "Failure"; readonly error: string }
Arrays and Tuples
import { Schema } from "effect"
// Arrays
const StringArray = Schema.Array(Schema.String)
// Tuples
const Pair = Schema.Tuple([Schema.String, Schema.Number])
// Tuples with rest elements
const TupleWithRest = Schema.TupleWithRest(
Schema.Tuple([Schema.String, Schema.Number]),
[Schema.Boolean]
)
Records
Records define dynamic key-value mappings:
import { Schema } from "effect"
// String keys, number values
const StringToNumber = Schema.Record(Schema.String, Schema.Number)
// Number keys
const IntToString = Schema.Record(Schema.Int, Schema.String)
Transformations convert values between types during decoding and encoding:
import { Schema, SchemaTransformation } from "effect"
// Transform string to uppercase during decoding
const Uppercase = Schema.String.decode(SchemaTransformation.toUpperCase())
// Custom transformation
const StringToNumber = Schema.String.pipe(
Schema.decodeTo(Schema.Number, {
decode: (s) => Effect.succeed(Number(s)),
encode: (n) => Effect.succeed(String(n))
})
)
Deriving Schemas
Struct Derivations
import { Schema, Struct } from "effect"
const User = Schema.Struct({
name: Schema.String,
email: Schema.String,
age: Schema.Number
})
// Pick specific fields
const NameOnly = User.mapFields(Struct.pick(["name"]))
// Omit fields
const WithoutAge = User.mapFields(Struct.omit(["age"]))
// Add fields
const WithRole = User.mapFields(
Struct.assign({ role: Schema.String })
)
// Make fields optional
const OptionalEmail = User.mapFields(
Struct.evolve({
email: (field) => Schema.optionalKey(field)
})
)
Tuple Derivations
import { Schema, Tuple } from "effect"
const Triple = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean])
// Pick elements
const FirstAndLast = Triple.mapElements(Tuple.pick([0, 2]))
// Add elements
const WithExtra = Triple.mapElements(
Tuple.appendElement(Schema.String)
)
Opaque Types and Classes
Create distinct TypeScript types backed by schemas:
import { Schema } from "effect"
class UserId extends Schema.Opaque<UserId>()(Schema.String) {}
class User extends Schema.Opaque<User>()(
Schema.Struct({
id: UserId,
name: Schema.String
})
) {}
const userId = UserId.makeUnsafe("user-123")
const user = User.makeUnsafe({ id: userId, name: "Alice" })
Template Literals
Define structured string patterns:
import { Schema } from "effect"
const Email = Schema.TemplateLiteral([
Schema.String.check(Schema.isMinLength(1)),
"@",
Schema.String.check(Schema.isMaxLength(64))
])
type Email = `${string}@${string}`
Validation and Parsing
Decoding
import { Schema } from "effect"
const User = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
// Synchronous decoding (throws on error)
const user = Schema.decodeUnknownSync(User)({ name: "Alice", age: 30 })
// Effect-based decoding
const program = Schema.decodeUnknown(User)({ name: "Alice", age: 30 })
// Exit-based decoding (returns Exit)
const exit = Schema.decodeUnknownExit(User)({ name: "Alice", age: 30 })
Encoding
import { Schema } from "effect"
const User = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
const user = { name: "Alice", age: 30 }
// Synchronous encoding
const encoded = Schema.encodeSync(User)(user)
// Effect-based encoding
const program = Schema.encode(User)(user)
Migration Strategy
When migrating Schema code from v3 to v4:
- Replace constraint constructors with
.check() — Convert Schema.minLength(5) to Schema.String.check(Schema.isMinLength(5))
- Update field definitions — Use
Schema.optionalKey and Schema.mutableKey for optional/mutable fields
- Explicit transformations — Use
SchemaTransformation module for type conversions
- Tagged unions — Consider
Schema.TaggedUnion for discriminated unions
- Review error messages — v4 provides clearer validation errors
Summary
Schema v4 represents a significant evolution in Effect’s validation and transformation capabilities:
- More explicit API with
.check() for validation rules
- Better tree-shaking and bundle size optimization
- Clearer distinction between optional and mutable fields
- Enhanced support for tagged unions and opaque types
- More predictable transformation behavior
While the migration requires updating existing schema definitions, the result is more maintainable and performant code.