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.
decodeUnknown 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)
// 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)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 SchemaIssue.Issue, 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 SchemaIssue.Issue 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.
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 })))Guards and assertions
Section titled “Guards and assertions”When you do not need a decoded value — only a yes/no answer — use Schema.is
for a type guard or Schema.asserts for an assertion. These run the schema’s
checks without producing the structured issue tree.
import { Schema } from "effect"
const NonEmpty = Schema.String.check(Schema.isNonEmpty())
const isNonEmpty = Schema.is(NonEmpty)
if (isNonEmpty("hello")) { // `"hello"` is narrowed to the schema's Type here.}Next steps
Section titled “Next steps”- Build up shapes with primitives and structs and records.
- Constrain values with filters.
- Read and present failures in error formatting.