# 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
  `Type` side, 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).

```ts
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" }
```
**Construct vs decode:** `make` works on the **decoded** `Type`. If a schema decodes a string into a
number (`Schema.FiniteFromString`), its constructor accepts a `number`, not a
`string` — decoding is what turns the string into the number. Constructor
defaults (`withConstructorDefault`) only fire during `make*`; decoding defaults
(`withDecodingDefault*`) only fire during decoding.

## 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.

### `make`

Constructs synchronously and **throws** a `SchemaError` (wrapped in an `Error`)
when validation fails. Best for trusted input, tests, and fixtures.

```ts
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 150
```

### `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.

```ts
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`

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.

```ts
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)
// => 42
```

### `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" }`).

```ts
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 })
// => 200
```

### `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.

```ts
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" }
```
**Defaults are shallow:** A constructor default on a *nested* struct field does not propagate to the
top-level `make`. Construct the nested value with the nested schema's own
constructor (`User.fields.address.make(...)`), or attach the default at the
level where you call `make`.

## Constructor defaults

`withConstructorDefault` attaches a default that is applied by `make*` (and by
[class constructors](https://effect.plants.sh/schema/classes/)) 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.

```ts
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"] }
```
**Pass an Effect, not a plain value:** In v4 the default is an `Effect`, not a raw value or a thunk: wrap a constant with
`Effect.succeed(...)`. Because it is an `Effect`, the default is re-run on every
construction — fresh arrays/objects each time, and side effects (an id, a
timestamp) recomputed per call.

### `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.

```ts
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.

```ts
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

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 with `optionalKey`: the key may
  be *absent* but not `undefined`. **Value-level** (no `Key`) wraps with
  `optional`: the key may be absent *or* explicitly `undefined`.
- **Encoded-default** (`withDecodingDefault*`) specifies the default as an
  `Encoded` value, run through the decode transformation.
  **Type-default** (`withDecodingDefaultType*`) specifies it as a decoded `Type`
  value, skipping the transformation.

All four accept `DecodingDefaultOptions` with `encodingStrategy`:
`"passthrough"` (default — keep the value when encoding) or `"omit"` (drop the
key when encoding).

### `withDecodingDefaultKey`

Key-level, encoded default: applies the default when the key is **absent** from
the input (not when it is `undefined`).

```ts
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 encode
Schema.encodeUnknownSync(S)({ name: "anonymous" })
// => { name: "anonymous" }
```

### `withDecodingDefault`

Value-level, encoded default: applies the default when the key is **absent or
`undefined`**.

```ts
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`

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.

```ts
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`

Value-level, **Type** default: like `withDecodingDefault`, but the default is a
decoded `Type` value, and it fires when the key is absent or `undefined`.

```ts
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 }
```
**Which one do I want?:** Pick **Key** vs value by whether explicit `undefined` should trigger the default
(value-level: yes; key-level: only true absence). Pick **Type** vs encoded by
where it is easier to express the default — as raw input (`Encoded`) or as the
final decoded value (`Type`).

## 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:

```ts
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](https://effect.plants.sh/services-and-layers/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.

```ts
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" }
```
**Constructor defaults are pure:** Only **decoding** defaults can read services — their `R` flows into
`DecodingServices`. `withConstructorDefault` takes an `Effect<MakeIn, SchemaError>`
with no service requirement, so a constructor default cannot depend on a service.
Put context-dependent defaults on the decoding side.

## Tags and discriminants

A common use of constructor + decoding defaults is auto-filling a discriminant on
a tagged union member.

### `tag`

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.

```ts
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`

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.

```ts
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`

Shorthand that adds a `_tag` field via `tag` automatically.
`Schema.TaggedStruct("A", { ... })` is equivalent to
`Schema.Struct({ _tag: Schema.tag("A"), ... })`.

```ts
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

| 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 |
**Related pages:** - [Structs and Records](https://effect.plants.sh/schema/structs-and-records/) — `optional`, `optionalKey`, and how missing keys are typed.
- [Classes](https://effect.plants.sh/schema/classes/) — `Schema.Class` constructors honor `withConstructorDefault`.
- [Services & Layers](https://effect.plants.sh/services-and-layers/services/) — defining the services that context-aware decoding defaults read from.