Schema
The Schema module provides a powerful way to define schemas that validate, parse, and transform data with compile-time type inference. Schemas bridge the gap between runtime and compile-time type safety.
Core Concepts
A schema describes:
- The Type (
T) - the validated/decoded output type
- The Encoded form (
E) - the input/encoded representation
- Decoding - transformation from encoded to type
- Encoding - transformation from type back to encoded
- Services - dependencies required for validation
Type Signatures
Schema
interface Schema<out T> extends Top {
readonly Type: T
}
The decoded/output type of the schema
Codec
interface Codec<out T, out E = T, out RD = never, out RE = never> extends Schema<T> {
readonly Encoded: E
readonly DecodingServices: RD
readonly EncodingServices: RE
}
The decoded type (output)
E
type parameter
default:"T"
The encoded type (input)
RD
type parameter
default:"never"
Services required for decoding
RE
type parameter
default:"never"
Services required for encoding
Primitive Schemas
String
A schema for string values.
import { Schema } from "effect"
const Name = Schema.String
// Schema<string>
Signature:
const String: Schema<string>
Number
A schema for number values.
const Age = Schema.Number
// Schema<number>
Signature:
const Number: Schema<number>
Boolean
A schema for boolean values.
const IsActive = Schema.Boolean
// Schema<boolean>
Signature:
const Boolean: Schema<boolean>
Composite Schemas
Struct
Defines an object schema with typed fields.
const User = Schema.Struct({
id: Schema.Number,
name: Schema.String,
email: Schema.String
})
// Schema<{ id: number; name: string; email: string }>
Signature:
function Struct<Fields extends Record<PropertyKey, Top>>(
fields: Fields
): Codec</* inferred type */, /* inferred encoded */>
fields
Record<PropertyKey, Top>
required
Object mapping field names to their schemas
A schema for objects with the specified fields
Array
Defines an array schema.
const Numbers = Schema.Array(Schema.Number)
// Schema<Array<number>>
Signature:
function Array<I, A>(
item: Schema<A, I>
): Schema<Array<A>, Array<I>>
Schema for array elements
Record
Defines a record/dictionary schema.
const StringToNumber = Schema.Record(
Schema.String,
Schema.Number
)
// Schema<Record<string, number>>
Signature:
function Record<K, V>(
key: Schema<K>,
value: Schema<V>
): Schema<Record<K, V>>
Union
Defines a union of schemas.
const StringOrNumber = Schema.Union(
Schema.String,
Schema.Number
)
// Schema<string | number>
Signature:
function Union<Members extends ReadonlyArray<Top>>(
...members: Members
): Codec</* union of types */, /* union of encoded */>
Tuple
Defines a tuple schema with fixed positions.
const Coordinate = Schema.Tuple(
Schema.Number,
Schema.Number
)
// Schema<[number, number]>
Signature:
function Tuple<Elements extends ReadonlyArray<Top>>(
...elements: Elements
): Codec</* tuple type */, /* tuple encoded */>
Refinements
filter
Adds a validation constraint to a schema.
const PositiveNumber = Schema.Number.pipe(
Schema.filter((n) => n > 0, {
message: () => "Must be positive"
})
)
Signature:
function filter<S extends Top>(
predicate: Predicate<Schema.Type<S>>,
options?: {
message?: (value: Schema.Type<S>) => string
}
): (self: S) => S
brand
Creates a branded type for nominal typing.
const UserId = Schema.Number.pipe(
Schema.brand("UserId")
)
// Schema<number & Brand<"UserId">>
Signature:
function brand<B extends string | symbol>(
brand: B
): <S extends Top>(self: S) => /* branded schema */
Transforms between encoded and decoded representations.
const DateFromString = Schema.transform(
Schema.String,
Schema.Date,
{
decode: (s) => new Date(s),
encode: (d) => d.toISOString()
}
)
Signature:
function transform<From extends Top, To extends Top>(
from: From,
to: To,
options: {
decode: (encoded: Schema.Type<From>) => Schema.Type<To>
encode: (type: Schema.Type<To>) => Schema.Type<From>
}
): Codec<Schema.Type<To>, Schema.Type<From>>
The source schema (encoded form)
The target schema (decoded form)
Function to transform from encoded to decoded
Function to transform from decoded to encoded
Parsing and Validation
decode
Decodes/validates input according to a schema.
import { Effect, Schema } from "effect"
const User = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
const program = Effect.gen(function*() {
const result = yield* Schema.decode(User)({
name: "Alice",
age: 30
})
console.log(result) // { name: "Alice", age: 30 }
})
Signature:
function decode<I, A>(
schema: Schema<A, I>
): (input: I, options?: ParseOptions) => Effect.Effect<A, SchemaError>
The schema to decode with
The input value to decode
Optional parsing configuration
An effect that produces the decoded value or fails with SchemaError
encode
Encodes a value back to its encoded representation.
const encoded = yield* Schema.encode(DateFromString)(
new Date("2024-01-01")
)
// "2024-01-01T00:00:00.000Z"
Signature:
function encode<I, A>(
schema: Schema<A, I>
): (value: A, options?: ParseOptions) => Effect.Effect<I, SchemaError>
decodeSync
Synchronously decodes a value, throwing on error.
const user = Schema.decodeSync(User)({
name: "Bob",
age: 25
})
Signature:
function decodeSync<I, A>(
schema: Schema<A, I>
): (input: I, options?: ParseOptions) => A
decodeSync throws if validation fails or if the schema requires async operations.
Annotations
annotate
Adds metadata annotations to a schema.
const Email = Schema.String.pipe(
Schema.annotate({
title: "Email Address",
description: "A valid email address",
examples: ["[email protected]"]
})
)
Signature:
function annotate<S extends Top>(
annotations: Annotations
): (self: S) => S
Metadata to attach to the schema (title, description, examples, etc.)
Built-in Refinements
NonEmpty
A string that must not be empty.
const NonEmptyString = Schema.NonEmpty
// Schema<string> (validated non-empty)
Positive
A number that must be positive.
const Age = Schema.Positive
// Schema<number> (validated > 0)
Int
An integer number.
const Count = Schema.Int
// Schema<number> (validated integer)
Option and Result
Option
Schema for Option values.
const MaybeString = Schema.Option(Schema.String)
// Schema<Option<string>>
Signature:
function Option<I, A>(
value: Schema<A, I>
): Schema<Option<A>, Option<I>>
Result
Schema for Result values.
const StringResult = Schema.Result({
success: Schema.String,
failure: Schema.Number
})
// Schema<Result<string, number>>
Signature:
function Result<S extends Top, F extends Top>(options: {
success: S
failure: F
}): Schema<Result<Schema.Type<S>, Schema.Type<F>>>
Schema.Type
Extracts the decoded type from a schema.
const User = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
type UserType = Schema.Type<typeof User>
// { name: string; age: number }
Codec.Encoded
Extracts the encoded type from a codec.
type Encoded = Schema.Codec.Encoded<typeof DateFromString>
// string
Complete Example
import { Effect, Schema } from "effect"
// Define a schema
const User = Schema.Struct({
id: Schema.Number.pipe(Schema.brand("UserId")),
name: Schema.NonEmpty,
email: Schema.String.pipe(
Schema.filter((s) => s.includes("@"), {
message: () => "Must be a valid email"
})
),
age: Schema.Number.pipe(
Schema.filter((n) => n >= 18, {
message: () => "Must be 18 or older"
})
),
role: Schema.Union(
Schema.Literal("admin"),
Schema.Literal("user"),
Schema.Literal("guest")
)
})
// Extract the type
type User = Schema.Type<typeof User>
// Use the schema
const program = Effect.gen(function*() {
// Decode/validate input
const user = yield* Schema.decode(User)({
id: 1,
name: "Alice",
email: "[email protected]",
age: 30,
role: "admin"
})
console.log(user)
// {
// id: 1 & Brand<"UserId">,
// name: "Alice",
// email: "[email protected]",
// age: 30,
// role: "admin"
// }
return user
})
Effect.runPromise(program)
.then(console.log)
.catch(error => console.error("Validation failed:", error))