Default Constructors & Defaults
There are two ways to produce a value that conforms to a schema:
- Decoding parses external input (
Encoded) — JSON from an API, a row from a database — running the full decode pipeline, including transformations. - Constructing builds a value in your own code from an already-typed make
input. No string-to-number parsing, no key remapping — just validation of the
Typeside, plus any constructor defaults.
Every schema carries a constructor family — make, makeOption, and
makeEffect — and a set of combinators for filling in missing fields. This page
covers both: how to construct, and how to attach defaults that fire during
construction or during decoding (including defaults computed from an Effect
that reads a service).
import { Effect, Schema } from "effect"
const User = Schema.Struct({ name: Schema.String, // `role` may be omitted from the constructor input; it defaults to "member". role: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("member")))})
// Construct from a typed value (validates, applies the default):const user = User.make({ name: "Alice" })// => { name: "Alice", role: "member" }
// Decode untrusted input (also runs transformations):const decoded = Schema.decodeUnknownSync(User)({ name: "Bob", role: "admin" })// => { name: "Bob", role: "admin" }The constructor family
Section titled “The constructor family”Each schema exposes three constructors. They all take the schema’s
make input (~type.make.in) and differ only in how they report a validation
failure.
Constructs synchronously and throws a SchemaError (wrapped in an Error)
when validation fails. Best for trusted input, tests, and fixtures.
import { Schema } from "effect"
const Age = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 150 }))
Age.make(42)// => 42
Age.make(200)// => throws: a value between 0 and 150makeOption
Section titled “makeOption”Constructs synchronously and returns an Option — Option.some on success,
Option.none on failure. Use it when you only care whether construction
succeeded and want no error details.
import { Schema, Option } from "effect"
const Age = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 150 }))
Age.makeOption(42)// => Option.some(42)
Age.makeOption(200)// => Option.none()makeEffect
Section titled “makeEffect”Constructs through an Effect, keeping any failure in the error channel as a
SchemaError. Reach for this when construction sits inside other effectful code
and you want to compose the failure instead of throwing.
import { Effect, Schema } from "effect"
const Age = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 150 }))
const program = Age.makeEffect(42)// program: Effect<number, SchemaError>
Effect.runSync(program)// => 42MakeOptions
Section titled “MakeOptions”All three accept an optional second argument. disableChecks: true skips
validation when you fully trust the input; parseOptions tunes error reporting
(e.g. collecting all issues with { errors: "all" }).
import { Schema } from "effect"
const Age = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 150 }))
// Skip validation — the out-of-range value is returned as-is.Age.make(200, { disableChecks: true })// => 200make.in differs from Type
Section titled “make.in differs from Type”The constructor input is not the same as the Type. Fields that are
optional, or that carry a constructor default, become optional in the make
input. This is the Struct.MakeIn computation: a field marked with-default
moves into the optional half of the input object.
import { Effect, Schema } from "effect"
const User = Schema.Struct({ id: Schema.String, // optional key — omittable in both Type and make input nickname: Schema.optionalKey(Schema.String), // has a constructor default — required in Type, optional in make input role: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("member")))})
// User["Type"] => { readonly id: string; readonly nickname?: string; readonly role: string }// make input accepts => { readonly id: string; readonly nickname?: string; readonly role?: string }
User.make({ id: "u_1" })// => { id: "u_1", role: "member" }Constructor defaults
Section titled “Constructor defaults”withConstructorDefault attaches a default that is applied by make* (and by
class constructors) when the field is omitted. The default
is an Effect<MakeIn, SchemaError>, so it is lazily evaluated — re-run on
every construction — and may itself fail validation.
import { Effect, Schema } from "effect"
const Event = Schema.Struct({ name: Schema.String, // Re-evaluated on every make — each value gets a fresh array. tags: Schema.Array(Schema.String).pipe( Schema.withConstructorDefault(Effect.succeed([])) )})
Event.make({ name: "launch" })// => { name: "launch", tags: [] }
Event.make({ name: "launch", tags: ["release"] })// => { name: "launch", tags: ["release"] }withConstructorDefault
Section titled “withConstructorDefault”Adds a constructor default to a field that does not already have one (enforced by
the WithoutConstructorDefault constraint). The field becomes optional in the
struct’s make input and is set to with-default in the schema’s type.
import { Effect, Schema } from "effect"
const Config = Schema.Struct({ host: Schema.String, port: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(8080)))})
Config.make({ host: "localhost" })// => { host: "localhost", port: 8080 }
// Defaults are not applied during decoding:Schema.decodeUnknownSync(Config)({ host: "localhost", port: 80 })// => { host: "localhost", port: 80 }// Schema.decodeUnknownSync(Config)({ host: "localhost" }) would FAIL — port is required when decoding.Constructor defaults are portable: reuse the same field in another struct and the default comes with it.
import { Effect, Schema } from "effect"
const A = Schema.Struct({ count: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0)))})
const B = Schema.Struct({ label: Schema.String, count: A.fields.count // carries the default along})
B.make({ label: "x" })// => { label: "x", count: 0 }Decoding defaults
Section titled “Decoding defaults”These combinators fill a missing field during decoding (and encoding round trips), leaving the constructor untouched. There are four, along two axes:
- Key-level (
...Key) wraps the encoded side withoptionalKey: the key may be absent but notundefined. Value-level (noKey) wraps withoptional: the key may be absent or explicitlyundefined. - Encoded-default (
withDecodingDefault*) specifies the default as anEncodedvalue, run through the decode transformation. Type-default (withDecodingDefaultType*) specifies it as a decodedTypevalue, skipping the transformation.
All four accept DecodingDefaultOptions with encodingStrategy:
"passthrough" (default — keep the value when encoding) or "omit" (drop the
key when encoding).
withDecodingDefaultKey
Section titled “withDecodingDefaultKey”Key-level, encoded default: applies the default when the key is absent from
the input (not when it is undefined).
import { Effect, Schema } from "effect"
const S = Schema.Struct({ name: Schema.String.pipe(Schema.withDecodingDefaultKey(Effect.succeed("anonymous")))})
Schema.decodeUnknownSync(S)({})// => { name: "anonymous" }
Schema.decodeUnknownSync(S)({ name: "Ada" })// => { name: "Ada" }
// passthrough (default): the value is kept on encodeSchema.encodeUnknownSync(S)({ name: "anonymous" })// => { name: "anonymous" }withDecodingDefault
Section titled “withDecodingDefault”Value-level, encoded default: applies the default when the key is absent or
undefined.
import { Effect, Schema } from "effect"
const S = Schema.Struct({ name: Schema.String.pipe( Schema.optional, Schema.withDecodingDefault(Effect.succeed("anonymous")) )})
Schema.decodeUnknownSync(S)({})// => { name: "anonymous" }
Schema.decodeUnknownSync(S)({ name: undefined })// => { name: "anonymous" } (also fires for explicit undefined)
Schema.decodeUnknownSync(S)({ name: "Ada" })// => { name: "Ada" }withDecodingDefaultTypeKey
Section titled “withDecodingDefaultTypeKey”Key-level, Type default: like withDecodingDefaultKey, but the default is
given as a decoded Type value, so it does not pass through the decode
transformation. Useful when the transformation would be awkward to invert.
import { Effect, Schema } from "effect"
const S = Schema.Struct({ // Encoded is a string, Type is a number. count: Schema.FiniteFromString.pipe( Schema.withDecodingDefaultTypeKey(Effect.succeed(0)) // default is a number (Type) )})
Schema.decodeUnknownSync(S)({})// => { count: 0 }
Schema.decodeUnknownSync(S)({ count: "5" })// => { count: 5 }withDecodingDefaultType
Section titled “withDecodingDefaultType”Value-level, Type default: like withDecodingDefault, but the default is a
decoded Type value, and it fires when the key is absent or undefined.
import { Effect, Schema } from "effect"
const S = Schema.Struct({ count: Schema.FiniteFromString.pipe( Schema.optional, Schema.withDecodingDefaultType(Effect.succeed(0)) )})
Schema.decodeUnknownSync(S)({ count: undefined })// => { count: 0 }Context-aware defaults
Section titled “Context-aware defaults”The decoding-default combinators take an Effect<…, SchemaError, R>, and that
R flows into the schema’s DecodingServices. This is new in v4: a default can
be produced by an Effect that reads from the environment — a timestamp from
Clock, or a value from one of your own services.
Clock is a built-in environment service (not a requirement), so a default that
reads it adds nothing to DecodingServices and decodes synchronously:
import { Effect, Schema, Clock } from "effect"
const Message = Schema.Struct({ text: Schema.String, // When `createdAt` is absent, read the current time from Clock. createdAt: Schema.Number.pipe(Schema.withDecodingDefaultKey(Clock.currentTimeMillis))})
// DecodingServices stays `never`; you can decode with the sync API.Schema.decodeUnknownSync(Message)({ text: "hi" })// => { text: "hi", createdAt: 1717000000000 } (value depends on the clock)
Schema.decodeUnknownSync(Message)({ text: "hi", createdAt: 1 })// => { text: "hi", createdAt: 1 }For your own service, define it with Context.Service (see
Services). Now R is non-empty, so the schema
gains a DecodingServices requirement: you must decode with the effectful
Schema.decodeUnknownEffect and provide the layer before running.
import { Effect, Schema, Context, Layer } from "effect"
// A service that mints sequential ids.class IdGen extends Context.Service<IdGen, { readonly next: Effect.Effect<string>}>()("app/IdGen") { static readonly layer = Layer.effect( IdGen, Effect.sync(() => { let n = 0 return IdGen.of({ next: Effect.sync(() => `id_${n++}`) }) }) )}
const Entity = Schema.Struct({ name: Schema.String, // Reach into the IdGen service, then read its `next` effect. id: Schema.String.pipe( Schema.withDecodingDefaultKey(IdGen.use((gen) => gen.next)) )})
// DecodingServices now includes IdGen, so decoding is an Effect requiring it:const program = Schema.decodeUnknownEffect(Entity)({ name: "widget" })// program: Effect<{ name: string; id: string }, SchemaError, IdGen>
Effect.runSync(program.pipe(Effect.provide(IdGen.layer)))// => { name: "widget", id: "id_0" }Tags and discriminants
Section titled “Tags and discriminants”A common use of constructor + decoding defaults is auto-filling a discriminant on a tagged union member.
Builds a Literal schema with a constructor default equal to that literal — so
the _tag field can be omitted in make and is auto-filled. Decoding and
encoding still require the tag to be present.
import { Schema } from "effect"
const A = Schema.Struct({ _tag: Schema.tag("A"), value: Schema.Number })
A.make({ value: 42 })// => { _tag: "A", value: 42 }
Schema.decodeUnknownSync(A)({ _tag: "A", value: 42 })// => { _tag: "A", value: 42 }tagDefaultOmit
Section titled “tagDefaultOmit”Like tag, but combined with withDecodingDefaultKey(..., { encodingStrategy: "omit" }):
the _tag is filled during decoding when missing, and omitted from encoded
output. Handy for wire formats that don’t carry the discriminant.
import { Schema } from "effect"
const A = Schema.Struct({ _tag: Schema.tagDefaultOmit("A"), value: Schema.Number })
// Decode can omit the tag — it is filled in:Schema.decodeUnknownSync(A)({ value: 1 })// => { _tag: "A", value: 1 }
// Encode strips the tag back out:Schema.encodeUnknownSync(A)({ _tag: "A", value: 1 })// => { value: 1 }TaggedStruct
Section titled “TaggedStruct”Shorthand that adds a _tag field via tag automatically.
Schema.TaggedStruct("A", { ... }) is equivalent to
Schema.Struct({ _tag: Schema.tag("A"), ... }).
import { Schema } from "effect"
const Move = Schema.TaggedStruct("Move", { dx: Schema.Number, dy: Schema.Number })
// _tag optional in make, auto-filled:Move.make({ dx: 1, dy: 2 })// => { _tag: "Move", dx: 1, dy: 2 }
// Access the literal:Move.fields._tag.schema.literal// => "Move"Reference summary
Section titled “Reference summary”| API | Fires during | Side / absence rule | Default given as |
|---|---|---|---|
withConstructorDefault | make* only | field omitted from make input | MakeIn value |
withDecodingDefaultKey | decode | key absent (not undefined) | Encoded value |
withDecodingDefault | decode | key absent or undefined | Encoded value |
withDecodingDefaultTypeKey | decode | key absent (not undefined) | Type value |
withDecodingDefaultType | decode | key absent or undefined | Type value |
tag | make* (fill) + required on decode/encode | — | literal |
tagDefaultOmit | make* + decode (fill), encode (omit) | key absent on decode | literal |