Structs and Records
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 and tuples for ordered collections.
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) )})With shapes in hand, the next step is constraining the values inside them with filters.