| name | effect-schema |
| user-invocable | false |
| description | Use when @effect/schema patterns including schema definition, validation, parsing, encoding, and transformations. Use for type-safe data validation in Effect applications. |
| allowed-tools | ["Bash","Read","Write","Edit"] |
Effect Schema
Master type-safe data validation and transformation with @effect/schema. This
skill covers schema definition, parsing, encoding, and advanced schema patterns
for building robust data pipelines.
Basic Schema Types
Primitive Schemas
import { Schema } from "@effect/schema"
const StringSchema = Schema.String
const NumberSchema = Schema.Number
const BooleanSchema = Schema.Boolean
const BigIntSchema = Schema.BigInt
const SymbolSchema = Schema.Symbol
const UndefinedSchema = Schema.Undefined
const VoidSchema = Schema.Void
const NullSchema = Schema.Null
const UnknownSchema = Schema.Unknown
const AnySchema = Schema.Any
Literal Values
import { Schema } from "@effect/schema"
const HelloSchema = Schema.Literal("hello")
const FortyTwoSchema = Schema.Literal(42)
const TrueSchema = Schema.Literal(true)
const StatusSchema = Schema.Literal("pending", "approved", "rejected")
Struct Schemas
Basic Struct
import { Schema } from "@effect/schema"
const UserSchema = Schema.Struct({
id: Schema.String,
name: Schema.String,
age: Schema.Number,
email: Schema.String
})
type User = Schema.Schema.Type<typeof UserSchema>
Optional Fields
import { Schema } from "@effect/schema"
const PersonSchema = Schema.Struct({
name: Schema.String,
age: Schema.Number,
email: Schema.optional(Schema.String),
phone: Schema.optional(Schema.String)
})
type Person = Schema.Schema.Type<typeof PersonSchema>
Nested Schemas
import { Schema } from "@effect/schema"
const AddressSchema = Schema.Struct({
street: Schema.String,
city: Schema.String,
zipCode: Schema.String
})
const UserWithAddressSchema = Schema.Struct({
id: Schema.String,
name: Schema.String,
address: AddressSchema
})
Arrays and Collections
Array Schemas
import { Schema } from "@effect/schema"
const StringArraySchema = Schema.Array(Schema.String)
const NumberArraySchema = Schema.Array(Schema.Number)
const UsersSchema = Schema.Array(UserSchema)
const NonEmptyStringArray = Schema.NonEmptyArray(Schema.String)
Tuple Schemas
import { Schema } from "@effect/schema"
const CoordinatesSchema = Schema.Tuple(
Schema.Number,
Schema.Number
)
const ResponseSchema = Schema.Tuple(
Schema.Number,
Schema.String,
Schema.optional(Schema.Unknown)
)
Record Schemas
import { Schema } from "@effect/schema"
const ScoresSchema = Schema.Record({
key: Schema.String,
value: Schema.Number
})
const ConfigSchema = Schema.Record({
key: Schema.TemplateLiteral("config.", Schema.String),
value: Schema.String
})
Union and Intersection
Union Types
import { Schema } from "@effect/schema"
const StringOrNumberSchema = Schema.Union(
Schema.String,
Schema.Number
)
const ShapeSchema = Schema.Union(
Schema.Struct({
kind: Schema.Literal("circle"),
radius: Schema.Number
}),
Schema.Struct({
kind: Schema.Literal("rectangle"),
width: Schema.Number,
height: Schema.Number
})
)
type Shape = Schema.Schema.Type<typeof ShapeSchema>
Intersection Types
import { Schema } from "@effect/schema"
const TimestampsSchema = Schema.Struct({
createdAt: Schema.Date,
updatedAt: Schema.Date
})
const UserWithTimestampsSchema = Schema.extend(
UserSchema,
TimestampsSchema
)
const ManualIntersection = Schema.Struct({
...UserSchema.fields,
...TimestampsSchema.fields
})
Parsing and Validation
Synchronous Parsing
import { Schema } from "@effect/schema"
const UserSchema = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
const parseUser = Schema.decodeUnknownSync(UserSchema)
try {
const user = parseUser({ name: "Alice", age: 30 })
console.log(user)
} catch (error) {
console.error("Validation failed:", error)
}
parseUser({ name: "Bob" })
Effect-Based Parsing
import { Schema } from "@effect/schema"
import { Effect } from "effect"
const parseUserEffect = Schema.decodeUnknown(UserSchema)
const program = Effect.gen(function* () {
const user = yield* parseUserEffect({ name: "Alice", age: 30 })
return user
})
const safeProgram = program.pipe(
Effect.catchTag("ParseError", (error) =>
Effect.sync(() => {
console.error("Parse error:", error.message)
return null
})
)
)
Encoding
import { Schema } from "@effect/schema"
const encodeUser = Schema.encodeSync(UserSchema)
const user: User = { name: "Alice", age: 30 }
const encoded = encodeUser(user)
Schema Transformations
Transform Schemas
import { Schema } from "@effect/schema"
const DateFromString = Schema.transform(
Schema.String,
Schema.Date,
{
decode: (s) => new Date(s),
encode: (d) => d.toISOString()
}
)
const parseDate = Schema.decodeUnknownSync(DateFromString)
const date = parseDate("2024-01-01T00:00:00Z")
const encodeDate = Schema.encodeSync(DateFromString)
const isoString = encodeDate(new Date())
Validated Transformations
import { Schema } from "@effect/schema"
import { ParseResult } from "@effect/schema/ParseResult"
const EmailSchema = Schema.transformOrFail(
Schema.String,
Schema.String,
{
decode: (s) => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) {
return ParseResult.fail(
ParseResult.type(Schema.String.ast, s, "Invalid email format")
)
}
return ParseResult.succeed(s.toLowerCase())
},
encode: (s) => ParseResult.succeed(s)
}
)
Refinements and Constraints
String Refinements
import { Schema } from "@effect/schema"
const UsernameSchema = Schema.String.pipe(
Schema.minLength(3),
Schema.maxLength(20)
)
const UUIDSchema = Schema.String.pipe(
Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
)
const EmailSchema = Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/))
const HttpUrlSchema = Schema.String.pipe(Schema.startsWith("http"))
Number Refinements
import { Schema } from "@effect/schema"
const PositiveSchema = Schema.Number.pipe(Schema.positive())
const AgeSchema = Schema.Number.pipe(
Schema.greaterThanOrEqualTo(0),
Schema.lessThanOrEqualTo(120)
)
const IntegerSchema = Schema.Number.pipe(Schema.int())
const EvenSchema = Schema.Number.pipe(Schema.multipleOf(2))
Custom Refinements
import { Schema } from "@effect/schema"
const PasswordSchema = Schema.String.pipe(
Schema.minLength(8),
Schema.filter((s) => ({
message: () => "Password must contain uppercase, lowercase, and number",
test: () =>
/[A-Z]/.test(s) &&
/[a-z]/.test(s) &&
/[0-9]/.test(s)
}))
)
Common Schema Patterns
API Response Schema
import { Schema } from "@effect/schema"
const ApiResponseSchema = <A, I, R>(dataSchema: Schema.Schema<A, I, R>) =>
Schema.Struct({
success: Schema.Boolean,
data: Schema.optional(dataSchema),
error: Schema.optional(Schema.String)
})
const UserResponseSchema = ApiResponseSchema(UserSchema)
type UserResponse = Schema.Schema.Type<typeof UserResponseSchema>
Paginated Response
import { Schema } from "@effect/schema"
const PaginatedSchema = <A, I, R>(itemSchema: Schema.Schema<A, I, R>) =>
Schema.Struct({
items: Schema.Array(itemSchema),
total: Schema.Number,
page: Schema.Number,
pageSize: Schema.Number
})
const PaginatedUsersSchema = PaginatedSchema(UserSchema)
Form Data Schema
import { Schema } from "@effect/schema"
const SignupFormSchema = Schema.Struct({
email: Schema.String.pipe(
Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
),
password: Schema.String.pipe(
Schema.minLength(8)
),
confirmPassword: Schema.String,
acceptTerms: Schema.Literal(true)
}).pipe(
Schema.filter((data) => ({
message: () => "Passwords must match",
test: () => data.password === data.confirmPassword
}))
)
Integration with Effect
Parsing in Effect Pipelines
import { Schema } from "@effect/schema"
import { Effect } from "effect"
const processUserData = (rawData: unknown) =>
Effect.gen(function* () {
const user = yield* Schema.decodeUnknown(UserSchema)(rawData)
const saved = yield* saveUser(user)
const notified = yield* sendNotification(user.email)
return saved
})
Handling Parse Errors
import { Schema } from "@effect/schema"
import { Effect } from "effect"
const safeParseUser = (data: unknown) =>
Schema.decodeUnknown(UserSchema)(data).pipe(
Effect.catchTag("ParseError", (error) =>
Effect.fail({
_tag: "ValidationError",
message: error.message,
errors: error.errors
})
)
)
Best Practices
-
Define Schemas Centrally: Keep schema definitions in a shared module.
-
Use Type Inference: Let TypeScript infer types from schemas with
Schema.Schema.Type.
-
Compose Small Schemas: Build complex schemas from smaller, reusable
pieces.
-
Validate at Boundaries: Parse external data at API boundaries.
-
Use Tagged Unions: Add discriminant fields for union types.
-
Document Schemas: Add JSDoc comments to schema definitions.
-
Test Schemas: Write tests for schema validation logic.
-
Use Transformations: Convert between internal and external
representations.
-
Handle Errors Gracefully: Provide meaningful error messages.
-
Version Schemas: Consider versioning for API schemas.
Common Pitfalls
-
Over-Validation: Validating internal data unnecessarily.
-
Weak Constraints: Not adding sufficient refinements.
-
Missing Optional Fields: Forgetting to mark optional fields.
-
Wrong Union Order: Putting general schemas before specific ones.
-
Not Handling Parse Errors: Assuming parsing always succeeds.
-
Circular References: Creating schemas with circular dependencies.
-
Performance: Validating large datasets synchronously.
-
Type Mismatches: Schema and TypeScript type definitions diverging.
-
Missing Transformations: Not transforming between formats.
-
Exposing Internal Types: Returning internal types from APIs.
When to Use This Skill
Use effect-schema when you need to:
- Validate API request/response data
- Parse configuration files
- Validate form inputs
- Transform data between formats
- Ensure data integrity at boundaries
- Generate TypeScript types from schemas
- Build type-safe data pipelines
- Validate database queries and results
- Parse environment variables
- Build robust data validation layers
Resources
Official Documentation
Related Skills
- effect-core-patterns - Basic Effect operations
- effect-error-handling - Handling parse errors