Skip to main content
Effect Schema provides a powerful way to define data structures with runtime validation, transformation, and serialization capabilities.

Overview

Schema is a core Effect module that enables:
  • Type-safe validation with automatic TypeScript inference
  • Data transformation between different representations
  • Serialization and deserialization
  • Error handling with detailed validation errors
  • Schema composition for complex data structures

Basic Schemas

Primitive Types

Define schemas for basic types:
import { Schema } from "effect"

// Basic primitives
const StringSchema = Schema.String
const NumberSchema = Schema.Number
const BooleanSchema = Schema.Boolean
const DateSchema = Schema.Date

// Use schemas to validate
const validateString = Schema.decode(StringSchema)

const result = validateString("hello") // Effect<string, SchemaError>

Struct Schemas

Define object structures:
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String,
  age: Schema.optional(Schema.Number),
  isActive: Schema.Boolean
})

type User = Schema.Type<typeof User>
// {
//   id: number
//   name: string
//   email: string
//   age?: number
//   isActive: boolean
// }

Array and Union Schemas

import { Schema } from "effect"

// Array of strings
const StringArray = Schema.Array(Schema.String)

// Union type
const Status = Schema.Union(
  Schema.Literal("pending"),
  Schema.Literal("approved"),
  Schema.Literal("rejected")
)

// Array of objects
const Users = Schema.Array(
  Schema.Struct({
    id: Schema.Number,
    name: Schema.String
  })
)

Validation

Decoding and Encoding

Schemas support bidirectional transformation:
import { Effect, Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String
})

const program = Effect.gen(function* () {
  const decode = Schema.decode(User)
  
  // Decode from unknown input
  const user = yield* decode({
    id: 1,
    name: "Alice",
    email: "[email protected]"
  })
  
  console.log(user)
  // { id: 1, name: "Alice", email: "[email protected]" }
})

Validation with Refinements

Add custom validation rules:
import { Schema } from "effect"

// Email validation
const Email = Schema.String.check(
  Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/),
  { message: () => "Invalid email format" }
)

// Age validation
const Age = Schema.Number.check(
  Schema.isBetween({ minimum: 0, maximum: 150 }),
  { message: () => "Age must be between 0 and 150" }
)

// Use in struct
const User = Schema.Struct({
  name: Schema.NonEmptyString,
  email: Email,
  age: Age
})

Handling Validation Errors

import { Effect, Schema } from "effect"

const User = Schema.Struct({
  name: Schema.NonEmptyString,
  age: Schema.Number
})

const program = Effect.gen(function* () {
  const result = yield* Schema.decode(User)({
    name: "",
    age: "not a number"
  }).pipe(
    Effect.catchTag("SchemaError", (error) =>
      Effect.gen(function* () {
        yield* Effect.logError("Validation failed:", error.message)
        return null
      })
    )
  )
  
  return result
})

Transformations

Schema Transformations

Transform data between different representations:
import { Schema } from "effect"

// Transform string to Date
const DateFromString = Schema.String.decodeTo(
  Schema.Date,
  {
    decode: (s) => new Date(s),
    encode: (d) => d.toISOString()
  }
)

// Transform number to boolean
const BooleanFromNumber = Schema.Number.decodeTo(
  Schema.Boolean,
  {
    decode: (n) => n !== 0,
    encode: (b) => b ? 1 : 0
  }
)

Complex Transformations

import { Effect, Schema } from "effect"

// Database representation
const UserEncoded = Schema.Struct({
  id: Schema.Number,
  full_name: Schema.String,
  email_address: Schema.String,
  created_at: Schema.String
})

// Application representation
const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String,
  createdAt: Schema.Date
})

// Transform between representations
const UserFromDb = UserEncoded.decodeTo(User, {
  decode: (encoded) => ({
    id: encoded.id,
    name: encoded.full_name,
    email: encoded.email_address,
    createdAt: new Date(encoded.created_at)
  }),
  encode: (user) => ({
    id: user.id,
    full_name: user.name,
    email_address: user.email,
    created_at: user.createdAt.toISOString()
  })
})

Tagged Schemas

Tagged Unions

Create discriminated unions with tags:
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)

type Result = Schema.Type<typeof Result>
// { _tag: "Success"; value: string }
// | { _tag: "Failure"; error: string }

Pattern Matching with Tags

import { Effect, Schema } from "effect"

const handleResult = (result: Result) => {
  switch (result._tag) {
    case "Success":
      return Effect.succeed(result.value)
    case "Failure":
      return Effect.fail(result.error)
  }
}

Schema Classes

TaggedErrorClass

Create error classes with schemas:
import { Effect, Schema } from "effect"

class ValidationError extends Schema.TaggedErrorClass<ValidationError>()("ValidationError", {
  field: Schema.String,
  message: Schema.String
}) {}

class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("NotFoundError", {
  id: Schema.Number,
  resource: Schema.String
}) {}

// Use in effects
const findUser = (id: number) =>
  Effect.gen(function* () {
    const user = yield* getUserFromDb(id)
    
    if (!user) {
      return yield* Effect.fail(
        new NotFoundError({ id, resource: "User" })
      )
    }
    
    return user
  })

Error Handling with Tagged Errors

import { Effect, Schema } from "effect"

const program = Effect.gen(function* () {
  const user = yield* findUser(123)
  return user
}).pipe(
  Effect.catchTag("NotFoundError", (error) =>
    Effect.gen(function* () {
      yield* Effect.logWarning(
        `${error.resource} with id ${error.id} not found`
      )
      return null
    })
  ),
  Effect.catchTag("ValidationError", (error) =>
    Effect.gen(function* () {
      yield* Effect.logError(
        `Validation failed for ${error.field}: ${error.message}`
      )
      return null
    })
  )
)

Class-Based Schemas

Schema Classes

Create class instances with validation:
import { Schema } from "effect"

class User extends Schema.Class<User>("User", {
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String
}) {
  get displayName() {
    return `${this.name} (${this.email})`
  }
}

// Create instances with validation
const user = new User({
  id: 1,
  name: "Alice",
  email: "[email protected]"
})

console.log(user.displayName)
// "Alice ([email protected])"

TaggedClass

Classes with discriminator tags:
import { Schema } from "effect"

class CreateUserCommand extends Schema.TaggedClass<CreateUserCommand>()("CreateUser", {
  name: Schema.String,
  email: Schema.String
}) {}

class UpdateUserCommand extends Schema.TaggedClass<UpdateUserCommand>()("UpdateUser", {
  id: Schema.Number,
  name: Schema.optional(Schema.String),
  email: Schema.optional(Schema.String)
}) {}

type Command = CreateUserCommand | UpdateUserCommand

const handleCommand = (command: Command) => {
  switch (command._tag) {
    case "CreateUser":
      return createUser(command.name, command.email)
    case "UpdateUser":
      return updateUser(command.id, command)
  }
}

Advanced Features

Optional and Default Values

import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String,
  role: Schema.optional(Schema.String, { default: () => "user" }),
  metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
})

type User = Schema.Type<typeof User>
// {
//   id: number
//   name: string
//   email: string
//   role: string // defaults to "user"
//   metadata?: Record<string, unknown>
// }

Brand Types

Create nominal types with brands:
import { Brand, Schema } 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.brand("Email"),
  Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/)
)

const User = Schema.Struct({
  id: UserId,
  email: Email
})

Recursive Schemas

Define self-referential schemas:
import { Schema } from "effect"

interface Category {
  id: number
  name: string
  parent?: Category
}

const Category: Schema.Schema<Category> = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  parent: Schema.optional(Schema.suspend(() => Category))
})

JSON Schema Generation

Generate JSON Schema from Effect schemas:
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String
})

const jsonSchema = Schema.toJsonSchema(User)

console.log(JSON.stringify(jsonSchema, null, 2))
// {
//   "type": "object",
//   "properties": {
//     "id": { "type": "number" },
//     "name": { "type": "string" },
//     "email": { "type": "string" }
//   },
//   "required": ["id", "name", "email"]
// }

Best Practices

Keep schema definitions near the code that uses them:
// models/user.ts
export const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String
})
export type User = Schema.Type<typeof User>
Create typed error classes with schema validation:
class ValidationError extends Schema.TaggedErrorClass<ValidationError>()()
  ("ValidationError", { field: Schema.String })
{}
Build complex schemas from simpler ones:
const Address = Schema.Struct({ street: Schema.String })
const User = Schema.Struct({ 
  name: Schema.String,
  address: Address 
})
Use decodeTo for representing data differently:
const DateFromString = Schema.String.decodeTo(Schema.Date, {
  decode: (s) => new Date(s),
  encode: (d) => d.toISOString()
})

SQL

Build type-safe SQL queries

Caching

Cache validated data efficiently

Build docs developers (and LLMs) love