Defining Schemas
Primitives
import { Schema } from "effect"
const StringSchema = Schema.String
const NumberSchema = Schema.Number
const BooleanSchema = Schema.Boolean
const BigIntSchema = Schema.BigInt
Literals
import { Schema } from "effect"
const StatusSchema = Schema.Literal("active", "inactive", "pending")
const VersionSchema = Schema.Literal(1, 2, 3)
Structs
import { Schema } from "effect"
const UserSchema = Schema.Struct({
id: Schema.Number,
name: Schema.String,
email: Schema.String,
age: Schema.optional(Schema.Number),
roles: Schema.Array(Schema.String)
})
type User = Schema.Schema.Type<typeof UserSchema>
// type User = {
// id: number
// name: string
// email: string
// age?: number
// roles: string[]
// }
Unions
import { Schema } from "effect"
const ResultSchema = Schema.Union(
Schema.Struct({
type: Schema.Literal("success"),
value: Schema.Number
}),
Schema.Struct({
type: Schema.Literal("error"),
message: Schema.String
})
)
Arrays and Tuples
import { Schema } from "effect"
const NumberArraySchema = Schema.Array(Schema.Number)
const CoordinateSchema = Schema.Tuple(Schema.Number, Schema.Number)
const MixedTupleSchema = Schema.Tuple(Schema.String, Schema.Number, Schema.Boolean)
Validation
Decoding (Parsing)
import { Effect, Schema } from "effect"
const UserSchema = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
const program = Effect.gen(function*() {
// Decode from unknown
const user = yield* Schema.decode(UserSchema)({
name: "Alice",
age: 30
})
console.log(user)
// { name: "Alice", age: 30 }
})
// Synchronous decoding
const result = Schema.decodeSync(UserSchema)({
name: "Bob",
age: 25
})
// With Either for error handling
import { Either } from "effect"
const either = Schema.decodeEither(UserSchema)({ name: "Charlie", age: "invalid" })
if (Either.isLeft(either)) {
console.error("Validation failed:", either.left)
} else {
console.log("Valid user:", either.right)
}
Encoding
import { Schema } from "effect"
const user = { name: "Alice", age: 30 }
const encoded = Schema.encodeSync(UserSchema)(user)
Validation
import { Schema } from "effect"
// Validate without transformation
const valid = Schema.validateSync(UserSchema)({
name: "Alice",
age: 30
})
Transformations
Simple Transformations
import { Schema } from "effect"
const TrimmedString = Schema.String.pipe(
Schema.transform(
Schema.String,
{ decode: (s) => s.trim(), encode: (s) => s }
)
)
const result = Schema.decodeSync(TrimmedString)(" hello ")
// "hello"
DateFromString
import { Schema } from "effect"
const DateFromISOString = Schema.transformOrFail(
Schema.String,
Schema.Date,
{
decode: (s, _, ast) => {
const date = new Date(s)
return isNaN(date.getTime())
? ParseResult.fail(new ParseResult.Type(ast, s))
: ParseResult.succeed(date)
},
encode: (date) => ParseResult.succeed(date.toISOString())
}
)
const EventSchema = Schema.Struct({
name: Schema.String,
date: DateFromISOString
})
NumberFromString
import { Schema } from "effect"
const NumberFromString = Schema.transformOrFail(
Schema.String,
Schema.Number,
{
decode: (s, _, ast) => {
const n = Number(s)
return isNaN(n)
? ParseResult.fail(new ParseResult.Type(ast, s))
: ParseResult.succeed(n)
},
encode: (n) => ParseResult.succeed(String(n))
}
)
Refinements
Built-in Refinements
import { Schema } from "effect"
const PositiveNumber = Schema.Number.pipe(Schema.positive())
const Email = Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/))
const NonEmptyString = Schema.String.pipe(Schema.minLength(1))
const BoundedNumber = Schema.Number.pipe(Schema.greaterThan(0), Schema.lessThan(100))
Custom Refinements
import { Schema, ParseResult } from "effect"
const EvenNumber = Schema.Number.pipe(
Schema.filter((n) => n % 2 === 0, {
message: () => "Expected an even number"
})
)
const Username = Schema.String.pipe(
Schema.filter((s) => {
if (s.length < 3) return "Username must be at least 3 characters"
if (s.length > 20) return "Username must be at most 20 characters"
if (!/^[a-zA-Z0-9_]+$/.test(s)) return "Username can only contain letters, numbers, and underscores"
return true
})
)
Brands
Create nominal types:import { Schema, Brand } from "effect"
type UserId = number & Brand.Brand<"UserId">
const UserId = Schema.Number.pipe(Schema.brand("UserId"))
type Email = string & Brand.Brand<"Email">
const Email = Schema.String.pipe(
Schema.pattern(/^[^@]+@[^@]+$/),
Schema.brand("Email")
)
const user = {
id: UserId.make(123),
email: Email.make("[email protected]")
}
// Type safety: can't mix up different IDs
type PostId = number & Brand.Brand<"PostId">
const PostId = Schema.Number.pipe(Schema.brand("PostId"))
// ❌ Type error: can't assign UserId to PostId
// const postId: PostId = user.id
Optional Fields
import { Schema } from "effect"
const UserSchema = Schema.Struct({
name: Schema.String,
age: Schema.optional(Schema.Number),
email: Schema.optional(Schema.String, { default: () => "[email protected]" }),
role: Schema.optional(Schema.String, { nullable: true })
})
type User = Schema.Schema.Type<typeof UserSchema>
// type User = {
// name: string
// age?: number
// email: string
// role?: string | null
// }
Advanced Schemas
Recursive Schemas
import { Schema } from "effect"
interface Category {
name: string
subcategories: ReadonlyArray<Category>
}
const CategorySchema: Schema.Schema<Category> = Schema.Struct({
name: Schema.String,
subcategories: Schema.Array(Schema.suspend(() => CategorySchema))
})
Extend
import { Schema } from "effect"
const PersonSchema = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
const EmployeeSchema = PersonSchema.pipe(
Schema.extend(Schema.Struct({
employeeId: Schema.Number,
department: Schema.String
}))
)
Pick and Omit
import { Schema } from "effect"
const UserSchema = Schema.Struct({
id: Schema.Number,
name: Schema.String,
email: Schema.String,
password: Schema.String
})
const PublicUserSchema = UserSchema.pipe(
Schema.omit("password")
)
const LoginSchema = UserSchema.pipe(
Schema.pick("email", "password")
)
Error Handling
import { Effect, Schema, ParseResult } from "effect"
const program = Effect.gen(function*() {
const result = yield* Schema.decode(UserSchema)(invalidData).pipe(
Effect.catchTag("ParseError", (error) =>
Effect.gen(function*() {
// Format error for display
const formatted = yield* ParseResult.ArrayFormatter.formatIssue(error.issue)
console.error("Validation errors:", formatted)
return defaultUser
})
)
)
})
Use
Schema.annotations() to add metadata like descriptions, examples, and JSON Schema properties for documentation generation.Schemas with transformations have both
Type (decoded) and Encoded forms. Use Schema.Schema.Type<S> for the decoded type and Schema.Schema.Encoded<S> for the encoded type.