Basic Usage
A schema describes both how to decode unknown input into a typed value and
how to encode that value back into its serialized form. Once you have a
schema you pick a runner — a function that turns the schema into an actual
parser tailored to how you want failures reported (throwing, Result, Effect,
and so on).
import { Schema } from "effect"
// The Encoded side stores dates as ISO strings (what arrives over the wire);// the decoded Type works with real `Date` values.const Event = Schema.Struct({ title: Schema.String, startsAt: Schema.DateFromString // Encoded: string -> Type: Date})
// `decodeUnknownSync` builds a parser for untrusted input and throws on failure.const event = Schema.decodeUnknownSync(Event)({ title: "Launch", startsAt: "2026-05-30T10:00:00.000Z"})
console.log(event.startsAt instanceof Date) // => true
// `encodeUnknownSync` runs the schema in reverse: Type -> Encoded.const encoded = Schema.encodeUnknownSync(Event)(event)
console.log(encoded)// => { title: "Launch", startsAt: "2026-05-30T10:00:00.000Z" }The two type parameters
Section titled “The two type parameters”Every schema is a codec parameterized by its decoded Type and its Encoded
representation. You can read these off any schema with the Type and Encoded
type accessors:
import { Schema } from "effect"
const Event = Schema.Struct({ title: Schema.String, startsAt: Schema.DateFromString})
// The value your program works with after decoding.type Event = typeof Event.Type// { readonly title: string; readonly startsAt: Date }
// The serialized shape that crosses the boundary.type EventEncoded = typeof Event.Encoded// { readonly title: string; readonly startsAt: string }For a plain Schema.String the two sides coincide (string to string).
Schemas like Schema.DateFromString, Schema.NumberFromString, or any custom
transformation make the two sides differ — that gap
is exactly what decoding and encoding bridge.
Schema, Codec, Decoder, Encoder
Section titled “Schema, Codec, Decoder, Encoder”A schema actually carries four type parameters. The fully-parameterized base is
Bottom, but in everyday code you work through four progressively narrower
views — each is just a structural interface that exposes a subset of the
parameters:
Schema<T>— tracks only the decodedType. Use it when you accept “any schema that decodes toT” and do not care about the encoded shape.Codec<T, E, RD, RE>— the full picture: decodedTypeT,EncodedtypeE, and the services required during decoding (RD) and encoding (RE).Decoder<T, RD>— a decode-only view (Encodedisunknown). Runner families likedecodeUnknown*accept anyDecoder.Encoder<E, RE>— an encode-only view (Typeisunknown). Theencode*families accept anyEncoder.
import { Schema } from "effect"
// Accept any schema that decodes to a string, ignoring its encoded shape.declare function print(schema: Schema.Schema<string>): voidprint(Schema.String) // okprint(Schema.NonEmptyString) // ok
// Accept any codec that decodes to T and encodes to a string.declare function serialize<T>(codec: Schema.Codec<T, string>): stringserialize(Schema.NumberFromString) // ok — decodes number, encoded as stringTop is the existential “any schema” type — every type parameter erased to
unknown. Use it only as the constraint for generic utilities that must accept
any schema.
There is also Optic<T, Iso>, a Schema<T> whose decode and encode require no
services (both service params are never), enabling lens/prism operations
without an Effect runtime. Most primitives implement it automatically.
revealCodec
Section titled “revealCodec”Widens a schema to the full Codec<T, E, RD, RE> interface so TypeScript infers
all four parameters. When a schema is stored in a variable typed as Schema<T>,
the encoded type and services are erased; revealCodec recovers them at zero
runtime cost.
import { Schema } from "effect"
const schema: Schema.Schema<number> = Schema.NumberFromString
const codec = Schema.revealCodec(schema)type Enc = typeof codec.Encoded // => stringrevealBottom
Section titled “revealBottom”Widens a schema to the fully-parameterized Bottom interface, exposing all of
its type parameters (ast, ~type.make.in, Iso, mutability/optionality, …)
for advanced introspection and generic schema utilities.
import { Schema } from "effect"
const bottom = Schema.revealBottom(Schema.String)type T = typeof bottom.Type // => stringtype E = typeof bottom.Encoded // => stringdecodeUnknown vs decode
Section titled “decodeUnknown vs decode”There are two families of runners:
decodeUnknown*acceptunknowninput. Use these at real boundaries where the input is untrusted (HTTP bodies,JSON.parseoutput, env values).decode*accept input already typed as the schema’sEncodedtype. Use these when an upstream layer has already established the encoded shape and you only need the decoding transformations to run.
The same split exists for encoding: encodeUnknown* accept unknown, encode*
accept the schema’s decoded Type.
Choosing a runner
Section titled “Choosing a runner”Each runner produces a different result shape so you can match it to the
boundary you are at. They all share the same signature shape —
decodeUnknownX(schema)(input).
import { Schema, Result } from "effect"
const Port = Schema.Number.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))
// 1. Sync — throws an Error (with the issue in its `cause`) on failure.const port = Schema.decodeUnknownSync(Port)(8080) // => 8080
// 2. Result — returns Result<number, SchemaIssue.Issue>, no exceptions.const result = Schema.decodeUnknownResult(Port)(99999)if (Result.isFailure(result)) { console.error(String(result.failure)) // human-readable issue}
// 3. Option — returns Option<number>, discarding the failure details.const maybe = Schema.decodeUnknownOption(Port)(-1) // => Option.none()Decoding inside Effect
Section titled “Decoding inside Effect”The runner you reach for most in application code is decodeUnknownEffect. It
keeps failures in the typed error channel as a SchemaError, and — unlike the
sync variants — it can run schemas whose transformations require services or
perform asynchronous work.
import { Schema, Effect } from "effect"
const Config = Schema.Struct({ retries: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)), endpoint: Schema.String})
// `Effect.fn` names the operation for tracing and returns an Effect.const loadConfig = Effect.fn("loadConfig")(function*(raw: unknown) { // Failures surface as SchemaError in the error channel. const config = yield* Schema.decodeUnknownEffect(Config)(raw) yield* Effect.log(`Loaded config for ${config.endpoint}`) return config})Because the failure lives in the error channel, you handle it with the usual
error-management combinators (Effect.catch,
Effect.catchTag, …) rather than try/catch. The SchemaError wraps the
structured issue tree in its .issue field; see
error formatting to render it.
Reporting every issue
Section titled “Reporting every issue”By default decoding stops at the first error. Pass { errors: "all" } as a
second argument to the runner to collect every issue in one pass — ideal for
form validation where you want to show all problems at once.
import { Schema } from "effect"
const Signup = Schema.Struct({ name: Schema.String.check(Schema.isNonEmpty()), age: Schema.Number.check(Schema.isGreaterThanOrEqualTo(18))})
// Collect all failures instead of bailing on the first one.const decode = Schema.decodeUnknownExit(Signup, { errors: "all" })
console.log(String(decode({ name: "", age: 12 })))Parse options
Section titled “Parse options”Every runner accepts an AST.ParseOptions object — either when the runner is
created (decodeUnknownX(schema, options)) or when it is applied
(decodeUnknownX(schema)(input, options)). Application options override creation
options.
| Option | Values | Default | Effect |
|---|---|---|---|
errors | "first" | "all" | "first" | Stop at the first error, or collect every error. |
onExcessProperty | "ignore" | "error" | "preserve" | "ignore" | Strip unknown object keys, fail on them, or keep them. |
propertyOrder | "none" | "original" | "none" | Let the system choose key order, or preserve input order. |
disableChecks | boolean | false | Skip validation checks while still applying defaults and transformations. |
concurrency | number | "unbounded" | 1 | Max async parse effects to run concurrently. |
import { Schema } from "effect"
const User = Schema.Struct({ id: Schema.Number, name: Schema.String })
// Fail when the input carries keys the schema does not declare.const strict = Schema.decodeUnknownResult(User, { onExcessProperty: "error" })
// Preserve the order in which keys appeared in the input.const ordered = Schema.decodeUnknownSync(User, { propertyOrder: "original" })Decode / encode entry points
Section titled “Decode / encode entry points”Each direction (decode and encode) comes in two input flavors — Unknown (for
unknown input) and the typed variant — and each flavor offers six result
shapes. That is 24 runners in total, all with the same call shape
runner(schema, options?)(input, options?).
The result shapes are:
Sync— returns the value; throws anError(with the issue incause) on failure.Effect— returns anEffect; failure is aSchemaErrorin the error channel. Required services are preserved, and async transformations work.Exit— returns anExit; failure is aSchemaErrorin the cause. Runs synchronously.Result— returns aResult; failure is aSchemaIssue.Issueas data.Option— returns anOption; discards failure details.Promise— returns aPromise; rejects with aSchemaIssue.Issue.
A schema and a sample value used by the examples below:
import { Schema } from "effect"
// Encoded: string <-> Type: numberconst Count = Schema.NumberFromStringDecoding unknown input
Section titled “Decoding unknown input”decodeUnknownSync
Section titled “decodeUnknownSync”Decodes unknown input synchronously, returning the decoded Type and throwing
an Error (with the issue in cause) on failure.
Schema.decodeUnknownSync(Count)("42") // => 42// Schema.decodeUnknownSync(Count)("x") // throws ErrordecodeUnknownEffect
Section titled “decodeUnknownEffect”Decodes unknown input into an Effect<Type, SchemaError, DecodingServices>,
preserving service requirements and supporting async transformations.
Schema.decodeUnknownEffect(Count)("42") // => Effect<number, SchemaError>decodeUnknownExit
Section titled “decodeUnknownExit”Decodes unknown input synchronously into Exit<Type, SchemaError>.
Schema.decodeUnknownExit(Count)("42") // => Exit.succeed(42)decodeUnknownResult
Section titled “decodeUnknownResult”Decodes unknown input synchronously into Result<Type, SchemaIssue.Issue>,
returning failures as data.
Schema.decodeUnknownResult(Count)("42") // => Result.succeed(42)Schema.decodeUnknownResult(Count)("x") // => Result.fail(<issue>)decodeUnknownOption
Section titled “decodeUnknownOption”Decodes unknown input synchronously into Option<Type>, discarding failure
details.
Schema.decodeUnknownOption(Count)("42") // => Option.some(42)Schema.decodeUnknownOption(Count)("x") // => Option.none()decodeUnknownPromise
Section titled “decodeUnknownPromise”Decodes unknown input into a Promise<Type> that rejects with a
SchemaIssue.Issue on failure. Only for service-free schemas.
Schema.decodeUnknownPromise(Count)("42") // => Promise<number> resolving to 42Decoding already-typed (Encoded) input
Section titled “Decoding already-typed (Encoded) input”These mirror the Unknown variants but accept input statically typed as the
schema’s Encoded type.
decodeSync
Section titled “decodeSync”Schema.decodeSync(Count)("42") // => 42 (input typed as string)decodeEffect
Section titled “decodeEffect”Schema.decodeEffect(Count)("42") // => Effect<number, SchemaError>decodeExit
Section titled “decodeExit”Schema.decodeExit(Count)("42") // => Exit.succeed(42)decodeResult
Section titled “decodeResult”Schema.decodeResult(Count)("42") // => Result.succeed(42)decodeOption
Section titled “decodeOption”Schema.decodeOption(Count)("42") // => Option.some(42)decodePromise
Section titled “decodePromise”Schema.decodePromise(Count)("42") // => Promise<number>Encoding from unknown input
Section titled “Encoding from unknown input”Encoding runs the schema in reverse (Type -> Encoded). The Unknown variants
accept unknown input.
encodeUnknownSync
Section titled “encodeUnknownSync”Schema.encodeUnknownSync(Count)(42) // => "42"encodeUnknownEffect
Section titled “encodeUnknownEffect”Schema.encodeUnknownEffect(Count)(42) // => Effect<string, SchemaError>encodeUnknownExit
Section titled “encodeUnknownExit”Schema.encodeUnknownExit(Count)(42) // => Exit.succeed("42")encodeUnknownResult
Section titled “encodeUnknownResult”Schema.encodeUnknownResult(Count)(42) // => Result.succeed("42")encodeUnknownOption
Section titled “encodeUnknownOption”Schema.encodeUnknownOption(Count)(42) // => Option.some("42")encodeUnknownPromise
Section titled “encodeUnknownPromise”Schema.encodeUnknownPromise(Count)(42) // => Promise<string>Encoding from already-typed (Type) input
Section titled “Encoding from already-typed (Type) input”These accept input statically typed as the schema’s decoded Type.
encodeSync
Section titled “encodeSync”Schema.encodeSync(Count)(42) // => "42"encodeEffect
Section titled “encodeEffect”Schema.encodeEffect(Count)(42) // => Effect<string, SchemaError>encodeExit
Section titled “encodeExit”Schema.encodeExit(Count)(42) // => Exit.succeed("42")encodeResult
Section titled “encodeResult”Schema.encodeResult(Count)(42) // => Result.succeed("42")encodeOption
Section titled “encodeOption”Schema.encodeOption(Count)(42) // => Option.some("42")encodePromise
Section titled “encodePromise”Schema.encodePromise(Count)(42) // => Promise<string>Validation without decoding
Section titled “Validation without decoding”When you do not need a decoded value — only a yes/no answer — use a guard or
assertion. These run the schema’s checks against the decoded Type side
without producing a value.
Builds a type guard (input) => input is T that returns true/false without
exposing issue details, narrowing the input on success.
import { Schema } from "effect"
const isString = Schema.is(Schema.String)
isString("hello") // => trueisString(42) // => false
const value: unknown = "hello"if (isString(value)) { console.log(value.toUpperCase()) // => "HELLO" (value narrowed to string)}asserts
Section titled “asserts”A TypeScript asserts function: returns normally when the input satisfies the
schema (narrowing it) and throws when it does not.
import { Schema } from "effect"
const input: unknown = "hello"Schema.asserts(Schema.String, input)console.log(input.toUpperCase()) // => "HELLO" (input narrowed)
// Schema.asserts(Schema.String, 123) // throwsisSchema
Section titled “isSchema”Returns true if a value is any schema (a Top). Useful when writing utilities
that branch on whether an argument is a schema.
import { Schema } from "effect"
Schema.isSchema(Schema.String) // => trueSchema.isSchema("not a schema") // => falseisSchemaError
Section titled “isSchemaError”Narrows an unknown value (e.g. a caught error) to a SchemaError, whose
.message renders the issue tree and whose .issue holds the structured tree.
import { Schema } from "effect"
try { Schema.decodeUnknownSync(Schema.Number)("not a number")} catch (err) { if (Schema.isSchemaError(err)) { console.log(err._tag) // => "SchemaError" }}Constructing values
Section titled “Constructing values”Every schema exposes a maker family that builds a decoded Type value
directly — no decoding/parsing of an encoded representation involved. Makers
apply constructor defaults and then run type-side validation.
The input is the schema’s ~type.make.in, which can differ from Type: fields
that have a constructor default become optional in make.in. Makers are methods
on the schema itself.
import { Effect, Schema } from "effect"
const Person = Schema.Struct({ name: Schema.String, // gives `age` a constructor default, so it is optional in make input age: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0)))})Constructs a Type value synchronously, throwing an Error (issue in cause)
when validation fails.
Person.make({ name: "Alice" })// => { name: "Alice", age: 0 } (age default applied)
// Person.make({ name: 123 } as any) // throws ErrormakeOption
Section titled “makeOption”Constructs a Type value synchronously, returning Option.some on success or
Option.none when validation fails.
Person.makeOption({ name: "Alice" }) // => Option.some({ name: "Alice", age: 0 })makeEffect
Section titled “makeEffect”Constructs a Type value inside an Effect, keeping validation failures in the
error channel as a SchemaError.
Person.makeEffect({ name: "Alice" })// => Effect<{ name: string; age: number }, SchemaError>Next steps
Section titled “Next steps”- Build up shapes with primitives and structs and records.
- Constrain values with filters.
- Pair decode/encode with transformations.
- Read and present failures in error formatting.