Skip to content

Structs, Records, Arrays & Tuples

Most real schemas are objects. Schema.Struct describes an object with a fixed set of known keys; Schema.Record describes a dictionary with a uniform key and value type. Around them sit arrays, tuples, and the Effect collection types (Chunk, HashMap, ReadonlySet, …) for ordered and keyed data.

import { Schema } from "effect"
// A struct with required, optional, and constrained fields.
const User = Schema.Struct({
id: Schema.String,
name: Schema.String.check(Schema.isMinLength(1)),
// Exact optional: the key may be omitted entirely, but not set to undefined.
email: Schema.optionalKey(Schema.String),
// A nested array of literals.
roles: Schema.Array(Schema.Literals(["admin", "member"]))
})
const user = Schema.decodeUnknownSync(User)({
id: "u_1",
name: "Alice",
roles: ["admin"]
})
console.log(user)
// { id: "u_1", name: "Alice", roles: ["admin"] }

A struct decodes an object key by key. By default every field is required and the resulting type is readonly. The decoded Type and Encoded types are derived from the field schemas:

import { Schema } from "effect"
const Point = Schema.Struct({
x: Schema.Number,
y: Schema.Number
})
type Point = typeof Point.Type
// { readonly x: number; readonly y: number }

There are two distinct notions of “optional”, and the difference matters:

  • Schema.optionalKey(S)exact optional. The key may be absent, but if present it must satisfy S. The type becomes key?: T.
  • Schema.optional(S) — the key may be absent or explicitly undefined. The type becomes key?: T | undefined. (It is shorthand for optionalKey(UndefinedOr(S)).)
import { Schema } from "effect"
const Profile = Schema.Struct({
name: Schema.String,
// absent OK, undefined NOT OK
nickname: Schema.optionalKey(Schema.String),
// absent OK, undefined also OK
bio: Schema.optional(Schema.String)
})
type Profile = typeof Profile.Type
// {
// readonly name: string
// readonly nickname?: string
// readonly bio?: string | undefined
// }

To supply a value when a key is absent during decoding, wrap the field with Schema.withDecodingDefaultKey. The default is provided as an Effect, so it can be computed (and can even use services).

import { Effect, Schema } from "effect"
const Settings = Schema.Struct({
// When `theme` is missing from the input, decode it as "light".
theme: Schema.String.pipe(
Schema.withDecodingDefaultKey(Effect.succeed("light"))
)
})
console.log(Schema.decodeUnknownSync(Settings)({}))
// { theme: "light" }

Struct fields are readonly by default. Use Schema.mutableKey to drop the readonly modifier on a single field:

import { Schema } from "effect"
const Counter = Schema.Struct({
label: Schema.String,
// produces `count: number` instead of `readonly count: number`
count: Schema.mutableKey(Schema.Number)
})

Schema.Record describes a dictionary: an object whose keys all share one schema and whose values all share another. Use it when the keys are not known ahead of time.

import { Schema } from "effect"
// { readonly [x: string]: number }
const Scores = Schema.Record(Schema.String, Schema.Number)
const scores = Schema.decodeUnknownSync(Scores)({ alice: 10, bob: 7 })
console.log(scores)
// { alice: 10, bob: 7 }

Schema.Array describes a ReadonlyArray of a single element schema. Schema.NonEmptyArray additionally requires at least one element and narrows the type to a non-empty tuple.

import { Schema } from "effect"
const Tags = Schema.Array(Schema.String)
type Tags = typeof Tags.Type // readonly string[]
const Coordinates = Schema.NonEmptyArray(Schema.Number)
type Coordinates = typeof Coordinates.Type // readonly [number, ...number[]]

Schema.Tuple describes a fixed-length, positionally-typed array. To allow additional trailing elements of a uniform type, use Schema.TupleWithRest.

import { Schema } from "effect"
// Exactly [string, number]
const Pair = Schema.Tuple([Schema.String, Schema.Number])
type Pair = typeof Pair.Type // readonly [string, number]
// [string, ...number[]] — a label followed by any number of values
const Row = Schema.TupleWithRest(
Schema.Tuple([Schema.String]),
[Schema.Number]
)

When you have several object shapes distinguished by a tag, Schema.TaggedUnion builds the union and the matching helpers in one step. Each key becomes the _tag literal of that variant.

import { Schema } from "effect"
const Shape = Schema.TaggedUnion({
Circle: { radius: Schema.Number },
Rectangle: { width: Schema.Number, height: Schema.Number }
})
// `match` is generated for you — exhaustive over the variants.
const area = Shape.match(
{ _tag: "Circle", radius: 5 },
{
Circle: (c) => Math.PI * c.radius ** 2,
Rectangle: (r) => r.width * r.height
}
)
console.log(area.toFixed(2)) // "78.54"

This pairs naturally with pattern matching and is the recommended way to model serializable variant data.

A schema that refers to itself must defer the self-reference with Schema.suspend, otherwise the definition would loop forever while being built.

import { Schema } from "effect"
interface Category {
readonly name: string
readonly subcategories: ReadonlyArray<Category>
}
const Category = Schema.Struct({
name: Schema.String,
// `suspend` delays evaluating `Category` until it is actually needed.
subcategories: Schema.Array(
Schema.suspend((): Schema.Codec<Category> => Category)
)
})

Everything below is a per-API reference. Each entry has a short description and a runnable snippet. The first group covers schema constructors, the next the struct field modifiers, and the last the Struct module helpers for reshaping plain objects (and the matching schema-level combinators).

Defines an object with a fixed set of known keys. Each field value is a schema; the decoded Type is a readonly object.

import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
console.log(Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }))
// => { name: "Alice", age: 30 }

Extends a struct with one or more Record index signatures. The decoded type is the struct intersected with every rest record, so it has both the fixed keys and the open-ended ones.

import { Schema } from "effect"
const Config = Schema.StructWithRest(
Schema.Struct({ id: Schema.Number }),
[Schema.Record(Schema.String, Schema.String)]
)
// { readonly id: number } & { readonly [x: string]: string }
type Config = typeof Config.Type
console.log(Schema.decodeUnknownSync(Config)({ id: 1, env: "prod" }))
// => { id: 1, env: "prod" }

A Struct shorthand that prepends a literal _tag field. Schema.TaggedStruct("A", fields) is exactly Schema.Struct({ _tag: Schema.tag("A"), ...fields }) — useful for the cases of a discriminated union.

import { Schema } from "effect"
const Move = Schema.TaggedStruct("Move", { dx: Schema.Number, dy: Schema.Number })
console.log(Schema.decodeUnknownSync(Move)({ _tag: "Move", dx: 1, dy: 2 }))
// => { _tag: "Move", dx: 1, dy: 2 }
// The `make` constructor fills in `_tag` for you.
console.log(Move.make({ dx: 1, dy: 2 }))
// => { _tag: "Move", dx: 1, dy: 2 }

Builds a union of TaggedStructs from a { Tag: fields } map and attaches match / guard helpers. See Discriminated unions above.

import { Schema } from "effect"
const Event = Schema.TaggedUnion({
Created: { id: Schema.String },
Deleted: { id: Schema.String, reason: Schema.String }
})
console.log(Schema.decodeUnknownSync(Event)({ _tag: "Created", id: "e1" }))
// => { _tag: "Created", id: "e1" }

A dictionary schema with a uniform key schema and value schema. The key schema may be String, Number, a literal union, a template literal, or a refined string.

import { Schema } from "effect"
const ByName = Schema.Record(Schema.String, Schema.Number)
console.log(Schema.decodeUnknownSync(ByName)({ a: 1, b: 2 }))
// => { a: 1, b: 2 }
// Restrict the allowed keys with a literal union.
const Flags = Schema.Record(
Schema.Literals(["read", "write"]),
Schema.Boolean
)
console.log(Schema.decodeUnknownSync(Flags)({ read: true, write: false }))
// => { read: true, write: false }

A filter (used with .check) that validates every own key of an object — including symbol keys — against the encoded side of a key schema. Pair it with Record or a Struct to constrain key shapes.

import { Schema } from "effect"
// Keys must match the literal union, values are numbers.
const Limits = Schema.Record(Schema.String, Schema.Number).check(
Schema.isPropertyNames(Schema.Literals(["min", "max"]))
)
console.log(Schema.decodeUnknownSync(Limits)({ min: 0, max: 10 }))
// => { min: 0, max: 10 }
// Schema.decodeUnknownSync(Limits)({ min: 0, other: 1 }) // throws: "other" not allowed

A ReadonlyArray of a single element schema. (Schema.Array is exported from the Array constructor; reference it as Schema.Array.)

import { Schema } from "effect"
const Names = Schema.Array(Schema.String)
console.log(Schema.decodeUnknownSync(Names)(["a", "b"]))
// => ["a", "b"]

Like Array but requires at least one element; the decoded type is readonly [T, ...T[]].

import { Schema } from "effect"
const Path = Schema.NonEmptyArray(Schema.String)
console.log(Schema.decodeUnknownSync(Path)(["root"]))
// => ["root"]
// Schema.decodeUnknownSync(Path)([]) // throws: array must have at least 1 element

Accepts either a single value or an array, and always decodes to an array. On encoding, a one-element array is written back as the bare element.

import { Schema } from "effect"
const Tags = Schema.ArrayEnsure(Schema.String)
console.log(Schema.decodeUnknownSync(Tags)("solo")) // => ["solo"]
console.log(Schema.decodeUnknownSync(Tags)(["a", "b"])) // => ["a", "b"]
console.log(Schema.encodeUnknownSync(Tags)(["solo"])) // => "solo"
console.log(Schema.encodeUnknownSync(Tags)(["a", "b"])) // => ["a", "b"]

An array schema that additionally requires all elements to be unique, using the element schema’s derived equivalence.

import { Schema } from "effect"
const Ids = Schema.UniqueArray(Schema.Number)
console.log(Schema.decodeUnknownSync(Ids)([1, 2, 3]))
// => [1, 2, 3]
// Schema.decodeUnknownSync(Ids)([1, 1]) // throws: duplicate element

Decodes a Chunk (Effect’s immutable sequence). The decoded Type is Chunk<T>; the Iso/array form is a plain array of elements.

import { Chunk, Schema } from "effect"
const Numbers = Schema.Chunk(Schema.Number)
const chunk = Schema.decodeUnknownSync(Numbers)(Chunk.make(1, 2, 3))
console.log(Chunk.toReadonlyArray(chunk))
// => [1, 2, 3]

Decodes a native ReadonlySet whose values conform to the element schema.

import { Schema } from "effect"
const Ids = Schema.ReadonlySet(Schema.Number)
const set = Schema.decodeUnknownSync(Ids)(new Set([1, 2, 2]))
console.log([...set])
// => [1, 2]

Decodes an Effect HashSet of the element schema (value-based equality).

import { HashSet, Schema } from "effect"
const Ids = Schema.HashSet(Schema.Number)
const set = Schema.decodeUnknownSync(Ids)(HashSet.make(1, 2, 3))
console.log(HashSet.size(set))
// => 3

Decodes a native ReadonlyMap with typed keys and values. The Iso form is an array of [key, value] tuples.

import { Schema } from "effect"
const Scores = Schema.ReadonlyMap(Schema.String, Schema.Number)
const map = Schema.decodeUnknownSync(Scores)(new Map([["a", 1], ["b", 2]]))
console.log([...map])
// => [["a", 1], ["b", 2]]

Decodes an Effect HashMap with typed keys and values (value-based key equality). The Iso form is an array of [key, value] tuples.

import { HashMap, Schema } from "effect"
const Scores = Schema.HashMap(Schema.String, Schema.Number)
const map = Schema.decodeUnknownSync(Scores)(HashMap.make(["a", 1], ["b", 2]))
console.log(HashMap.get(map, "a"))
// => { _tag: "Some", value: 1 }

A fixed-length, positionally-typed array.

import { Schema } from "effect"
const Pair = Schema.Tuple([Schema.String, Schema.Number])
console.log(Schema.decodeUnknownSync(Pair)(["hello", 42]))
// => ["hello", 42]

A leading tuple followed by a uniform “rest” of trailing elements. The rest is an array of schemas, supporting both a rest element and fixed trailing elements.

import { Schema } from "effect"
// [string, number, ...boolean[]]
const Row = Schema.TupleWithRest(
Schema.Tuple([Schema.String, Schema.Number]),
[Schema.Boolean]
)
console.log(Schema.decodeUnknownSync(Row)(["x", 1, true, false]))
// => ["x", 1, true, false]

Makes an array or tuple schema mutable, removing the readonly modifier from the decoded type.

import { Schema } from "effect"
const Mutable = Schema.mutable(Schema.Array(Schema.Number))
type Mutable = typeof Mutable.Type // number[] (no readonly)

These wrap a single field inside a Struct. They control optionality and mutability of the decoded key; encodeKeys is the only one that changes the encoded key name.

Exact optional: the key may be absent, but if present must satisfy the inner schema. Produces key?: T.

import { Schema } from "effect"
const S = Schema.Struct({ name: Schema.optionalKey(Schema.String) })
type S = typeof S.Type // { readonly name?: string }
console.log(Schema.decodeUnknownSync(S)({})) // => {}
console.log(Schema.decodeUnknownSync(S)({ name: "a" })) // => { name: "a" }

The key may be absent or explicitly undefined. Shorthand for optionalKey(UndefinedOr(S)); produces key?: T | undefined.

import { Schema } from "effect"
const S = Schema.Struct({ name: Schema.optional(Schema.String) })
type S = typeof S.Type // { readonly name?: string | undefined }
console.log(Schema.decodeUnknownSync(S)({ name: undefined }))
// => { name: undefined }

Reverses optionalKey, returning the inner schema as a required field.

import { Schema } from "effect"
const opt = Schema.optionalKey(Schema.String)
const req = Schema.requiredKey(opt)
const S = Schema.Struct({ name: req })
type S = typeof S.Type // { readonly name: string }

Reverses optional, unwrapping the UndefinedOr member that optional added.

import { Schema } from "effect"
const opt = Schema.optional(Schema.String)
const req = Schema.required(opt)
const S = Schema.Struct({ name: req })
type S = typeof S.Type // { readonly name: string }

Removes the readonly modifier on a single field’s decoded property. Reverse with readonlyKey.

import { Schema } from "effect"
const S = Schema.Struct({ count: Schema.mutableKey(Schema.Number) })
type S = typeof S.Type // { count: number } (not readonly)

Reverses mutableKey, restoring the readonly modifier.

import { Schema } from "effect"
const mut = Schema.mutableKey(Schema.Number)
const ro = Schema.readonlyKey(mut)
const S = Schema.Struct({ count: ro })
type S = typeof S.Type // { readonly count: number }

Renames keys in the encoded form without touching the decoded type. Takes a partial { decodedKey: encodedKey } mapping and produces a transformation in both directions.

import { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
const Wire = Person.pipe(Schema.encodeKeys({ name: "full_name" }))
// decode: encoded key -> decoded key
console.log(Schema.decodeUnknownSync(Wire)({ full_name: "Alice", age: 30 }))
// => { name: "Alice", age: 30 }
// encode: decoded key -> encoded key
console.log(Schema.encodeUnknownSync(Wire)({ name: "Alice", age: 30 }))
// => { full_name: "Alice", age: 30 }

A Struct exposes its field map so you can reuse and transform fields. fields is the raw map (spread it); mapFields rebuilds a struct after running a function over the whole map — typically one of the Struct module helpers below.

The struct’s field definitions. Spread them into a new struct to reuse fields across schemas.

import { Schema } from "effect"
const Timestamped = Schema.Struct({
createdAt: Schema.Date,
updatedAt: Schema.Date
})
const User = Schema.Struct({
...Timestamped.fields,
name: Schema.String
})
// { readonly createdAt: Date; readonly updatedAt: Date; readonly name: string }

Returns a new struct after transforming the entire field map with a function. This is the entry point for the Struct module reshaping helpers.

import { Schema, Struct } from "effect"
const Full = Schema.Struct({
id: Schema.String,
name: Schema.String,
secret: Schema.String
})
// Drop a field by mapping the field map through Struct.omit.
const Public = Full.mapFields(Struct.omit(["secret"]))
type Public = typeof Public.Type // { readonly id: string; readonly name: string }

Creates a new object containing only the listed keys. With mapFields, narrows a schema’s fields.

import { Struct } from "effect"
console.log(Struct.pick({ a: 1, b: 2, c: 3 }, ["a", "c"]))
// => { a: 1, c: 3 }

Creates a new object with the listed keys removed.

import { Struct } from "effect"
console.log(Struct.omit({ a: 1, b: 2, c: 3 }, ["b"]))
// => { a: 1, c: 3 }

Merges two objects; on overlapping keys the second wins (type-level { ...a, ...b }).

import { Struct } from "effect"
console.log(Struct.assign({ theme: "light", lang: "en" }, { theme: "dark" }))
// => { theme: "dark", lang: "en" }

Use it with mapFields to add fields to a schema:

import { Schema, Struct } from "effect"
const Base = Schema.Struct({ id: Schema.String })
const WithName = Base.mapFields(Struct.assign({ name: Schema.String }))
// { readonly id: string; readonly name: string }

Transforms selected values with per-key functions; untouched keys are copied as-is. Return types may differ from the inputs.

import { Struct } from "effect"
console.log(
Struct.evolve({ name: "alice", age: 30 }, {
name: (s) => s.toUpperCase(),
age: (n) => n + 1
})
)
// => { name: "ALICE", age: 31 }

Transforms selected keys with per-key functions; values are unchanged.

import { Struct } from "effect"
console.log(
Struct.evolveKeys({ name: "Alice", age: 30 }, {
name: (k) => k.toUpperCase()
})
)
// => { NAME: "Alice", age: 30 }

Transforms both key and value together; each function returns a [newKey, newValue] tuple.

import { Struct } from "effect"
console.log(
Struct.evolveEntries({ amount: 100, label: "total" }, {
amount: (k, v) => [`${k}Cents`, v * 100],
label: (k, v) => [k, v.toUpperCase()]
})
)
// => { amountCents: 10000, label: "TOTAL" }

Renames keys via a static { oldKey: newKey } mapping; unmentioned keys are kept.

import { Struct } from "effect"
console.log(
Struct.renameKeys({ firstName: "Alice", lastName: "Smith" }, {
firstName: "first",
lastName: "last"
})
)
// => { first: "Alice", last: "Smith" }

Applies one typed Lambda to every value. The lambda must be created with Struct.lambda so the compiler can track output types.

import { pipe, Struct } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(pipe({ x: 1, y: 2 }, Struct.map(asArray)))
// => { x: [1], y: [2] }

Like Struct.map, but applies the lambda only to the listed keys.

import { pipe, Struct } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(pipe({ x: 1, y: 2, z: 3 }, Struct.mapPick(["x", "z"], asArray)))
// => { x: [1], y: 2, z: [3] }

Like Struct.map, but applies the lambda to all keys except the listed ones.

import { pipe, Struct } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(pipe({ x: 1, y: 2, z: 3 }, Struct.mapOmit(["y"], asArray)))
// => { x: [1], y: 2, z: [3] }

A struct-mapping lambda that adds fields. It is a shortcut for mapFields(Struct.assign(fields)), and works on every struct member of a union via mapMembers.

import { Schema, Tuple } from "effect"
// Add `c` to all members of a union of structs.
const Tagged = Schema.Union([
Schema.Struct({ a: Schema.String }),
Schema.Struct({ b: Schema.Number })
]).mapMembers(Tuple.map(Schema.fieldsAssign({ c: Schema.Number })))

Adds computed fields to a struct during decoding. Each derived field returns an Option; on encoding the derived fields are stripped, so they live only in the decoded type.

import { Option, Schema } from "effect"
const Person = Schema.Struct({ first: Schema.String, last: Schema.String })
const Extended = Person.pipe(
Schema.extendTo(
{ fullName: Schema.String },
{ fullName: (p) => Option.some(`${p.first} ${p.last}`) }
)
)
const alice = Schema.decodeUnknownSync(Extended)({ first: "Alice", last: "Smith" })
console.log(alice.fullName)
// => "Alice Smith"

Defers evaluation of a schema until it is needed, which is required for any schema that references itself.

import { Schema } from "effect"
interface Tree {
readonly value: number
readonly children: ReadonlyArray<Tree>
}
const Tree = Schema.Struct({
value: Schema.Number,
children: Schema.Array(Schema.suspend((): Schema.Codec<Tree> => Tree))
})

For nominal object types backed by a class (with methods, instanceof, and a constructor), reach for classes. To convert structs and records to and from wire formats — JSON strings, form data, URL search params — see transformations. With shapes in hand, the next step is constraining the values inside them with filters.