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"] }Structs
Section titled “Structs”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 }Optional fields
Section titled “Optional fields”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 satisfyS. The type becomeskey?: T.Schema.optional(S)— the key may be absent or explicitlyundefined. The type becomeskey?: T | undefined. (It is shorthand foroptionalKey(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// }Defaults for missing keys
Section titled “Defaults for missing keys”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" }Mutable fields
Section titled “Mutable fields”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)})Records
Section titled “Records”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 }Arrays
Section titled “Arrays”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[]]Tuples
Section titled “Tuples”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 valuesconst Row = Schema.TupleWithRest( Schema.Tuple([Schema.String]), [Schema.Number])Discriminated unions
Section titled “Discriminated unions”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.
Recursive schemas
Section titled “Recursive schemas”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) )})Reference
Section titled “Reference”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).
Object schemas
Section titled “Object schemas”Struct
Section titled “Struct”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 }StructWithRest
Section titled “StructWithRest”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" }TaggedStruct
Section titled “TaggedStruct”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 }TaggedUnion
Section titled “TaggedUnion”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" }Records & dictionaries
Section titled “Records & dictionaries”Record
Section titled “Record”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 }isPropertyNames
Section titled “isPropertyNames”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 allowedCollections
Section titled “Collections”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"]NonEmptyArray
Section titled “NonEmptyArray”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 elementArrayEnsure
Section titled “ArrayEnsure”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"]UniqueArray
Section titled “UniqueArray”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 elementDecodes 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]ReadonlySet
Section titled “ReadonlySet”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]HashSet
Section titled “HashSet”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))// => 3ReadonlyMap
Section titled “ReadonlyMap”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]]HashMap
Section titled “HashMap”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 }Tuples
Section titled “Tuples”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]TupleWithRest
Section titled “TupleWithRest”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]mutable
Section titled “mutable”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)Field modifiers
Section titled “Field modifiers”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.
optionalKey
Section titled “optionalKey”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" }optional
Section titled “optional”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 }requiredKey
Section titled “requiredKey”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 }required
Section titled “required”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 }mutableKey
Section titled “mutableKey”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)readonlyKey
Section titled “readonlyKey”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 }encodeKeys
Section titled “encodeKeys”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 keyconsole.log(Schema.decodeUnknownSync(Wire)({ full_name: "Alice", age: 30 }))// => { name: "Alice", age: 30 }
// encode: decoded key -> encoded keyconsole.log(Schema.encodeUnknownSync(Wire)({ name: "Alice", age: 30 }))// => { full_name: "Alice", age: 30 }Reshaping a struct
Section titled “Reshaping a struct”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.
Struct.fields
Section titled “Struct.fields”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 }Struct.mapFields
Section titled “Struct.mapFields”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 }Struct.pick
Section titled “Struct.pick”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 }Struct.omit
Section titled “Struct.omit”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 }Struct.assign
Section titled “Struct.assign”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 }Struct.evolve
Section titled “Struct.evolve”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 }Struct.evolveKeys
Section titled “Struct.evolveKeys”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 }Struct.evolveEntries
Section titled “Struct.evolveEntries”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" }Struct.renameKeys
Section titled “Struct.renameKeys”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" }Struct.map
Section titled “Struct.map”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] }Struct.mapPick
Section titled “Struct.mapPick”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] }Struct.mapOmit
Section titled “Struct.mapOmit”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] }Schema.fieldsAssign
Section titled “Schema.fieldsAssign”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 })))Schema.extendTo
Section titled “Schema.extendTo”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"Recursive & self-referential
Section titled “Recursive & self-referential”suspend
Section titled “suspend”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.