Skip to content

Derivations

A schema is a complete, machine-readable description of your data. Because of that, Effect can derive other artifacts directly from it — you write the schema once and get an equivalence, a test-data generator, a human formatter, a JSON codec, a JSON Patch differ, JSON Schema, and Standard Schema interop, all for free.

Every derivation lives on the Schema namespace as a to* function. Each one takes a schema and returns a plain value (an Equivalence, a FastCheck.Arbitrary, a function, a Codec, a Differ, etc.) that you can use anywhere.

The common case: one schema, many artifacts

Section titled “The common case: one schema, many artifacts”

Define a schema once, then derive whatever you need from it.

import { Schema } from "effect"
import * as FastCheck from "fast-check"
const Person = Schema.Struct({
id: Schema.Number,
name: Schema.String
})
// Structural equality
const eq = Schema.toEquivalence(Person)
eq({ id: 1, name: "Alice" }, { id: 1, name: "Alice" }) // => true
eq({ id: 1, name: "Alice" }, { id: 2, name: "Alice" }) // => false
// Random test data for property-based tests
const arb = Schema.toArbitrary(Person)
FastCheck.sample(arb, 1)[0] // => { id: -12, name: "..." } (random)
// Human-readable formatting
const format = Schema.toFormatter(Person)
format({ id: 1, name: "Alice" }) // => '{ "id": 1, "name": "Alice" }'
// A canonical JSON codec (Type <-> Json)
const json = Schema.toCodecJson(Person)
// A JSON Schema document
const jsonSchema = Schema.toJsonSchemaDocument(Person)

These derivations are structural: they walk the schema’s AST and recurse into structs, arrays, unions, and declarations. When the default behaviour is not right for a given type, attach a custom one with the matching overrideTo* helper (see Overriding a derivation).

Derives an Equivalence.Equivalence<T> that compares two values field-by-field according to the schema structure. Nested structs, arrays, and unions are compared recursively.

import { Schema } from "effect"
const eq = Schema.toEquivalence(
Schema.Struct({ id: Schema.Number, name: Schema.String })
)
eq({ id: 1, name: "Alice" }, { id: 1, name: "Alice" }) // => true
eq({ id: 1, name: "Alice" }, { id: 2, name: "Alice" }) // => false

Attaches a custom equivalence to a schema so that toEquivalence uses it instead of the derived structural comparison. Useful when two values should be treated as equal even though they are not field-by-field identical.

import { Schema } from "effect"
// Compare names case-insensitively
const Name = Schema.String.pipe(
Schema.overrideToEquivalence(() => (a, b) => a.toLowerCase() === b.toLowerCase())
)
const eq = Schema.toEquivalence(Name)
eq("Alice", "alice") // => true
eq("Alice", "Bob") // => false

Derived arbitraries are fast-check Arbitrary values, ready to feed into property-based tests. See Writing tests for how to run them with @effect/vitest.

Derives a FastCheck.Arbitrary<T> that generates values satisfying the schema (including any refinements such as length or range checks). Use this when you just need the arbitrary directly.

import { Schema } from "effect"
import * as FastCheck from "fast-check"
const PersonArb = Schema.toArbitrary(
Schema.Struct({ name: Schema.String, age: Schema.Number })
)
const sample = FastCheck.sample(PersonArb, 1)[0]
typeof sample.name // => "string"
typeof sample.age // => "number"

Derives a LazyArbitrary<T> — a function (fc) => FastCheck.Arbitrary<T> — that defers instantiation until you pass it the fast-check module. Prefer this when you need to control which fast-check instance is used, or for recursive schemas. The result is memoized, so repeated calls are cheap.

import { Schema } from "effect"
import * as FastCheck from "fast-check"
const lazy = Schema.toArbitraryLazy(Schema.String)
const arb = lazy(FastCheck) // => FastCheck.Arbitrary<string>
FastCheck.sample(arb, 1)[0] // => "..." (random string)

Derives a Formatter<T> — a function (value: T) => string — that renders a value as a human-readable string, recursing into structs, arrays, and unions. Strings are quoted, bigint literals get an n suffix, Option shows as some(...) / none(), and so on. This is distinct from issue/error formatting, which renders validation failures.

import { Schema } from "effect"
const format = Schema.toFormatter(
Schema.Struct({ id: Schema.Number, name: Schema.String })
)
format({ id: 1, name: "Alice" }) // => '{ "id": 1, "name": "Alice" }'
Schema.toFormatter(Schema.String)("a") // => '"a"'
Schema.toFormatter(Schema.BigInt)(1n) // => "1n"

The optional onBefore hook lets you intercept specific AST nodes before the default formatting runs:

import { Schema } from "effect"
const format = Schema.toFormatter(Schema.Number, {
onBefore: (ast) => (ast._tag === "Number" ? (n) => `#${n}` : undefined)
})
format(42) // => "#42"

Attaches a custom formatter to a schema so that toFormatter uses it instead of the structural default.

import { Schema } from "effect"
const Money = Schema.Number.pipe(
Schema.overrideToFormatter(() => (cents) => `$${(cents / 100).toFixed(2)}`)
)
Schema.toFormatter(Money)(1299) // => "$12.99"

Derives a SchemaRepresentation.Document — an intermediate, structural representation of the schema (a representation plus its named references). This is the IR that toJsonSchemaDocument is built on; reach for it directly when you want to walk or transform the schema’s shape yourself.

import { Schema } from "effect"
const doc = Schema.toRepresentation(
Schema.Struct({ name: Schema.String })
)
doc.representation // => the structural representation
doc.references // => named references ($ref map)

These derivations produce a new Codec whose encoded side is a canonical serialization format. The decoded (Type) side is unchanged, so you can decode and encode with the usual parsing APIs (Schema.encodeSync, Schema.decodeSync, etc.).

Derives a canonical Codec<T, Json> — encoding produces a JSON-compatible value and decoding reconstructs the schema’s Type. Types that are not natively JSON (dates, URL, bigint, Map, Set, …) are encoded through their built-in JSON representation.

import { Schema } from "effect"
const codec = Schema.toCodecJson(
Schema.Struct({ when: Schema.Date, name: Schema.String })
)
const json = Schema.encodeSync(codec)({
when: new Date("2024-01-01T00:00:00.000Z"),
name: "Alice"
})
// => { when: "2024-01-01T00:00:00.000Z", name: "Alice" }
Schema.decodeSync(codec)(json)
// => { when: Date(2024-01-01...), name: "Alice" }

Derives an isomorphic Codec<Type, Iso>. The encoded side is the schema’s Iso type — the intermediate representation used for lossless round-tripping (no information is lost in either direction). For most schemas the Iso and Type sides coincide; declarations may define a richer Iso form.

import { Schema } from "effect"
const codec = Schema.toCodecIso(Schema.Option(Schema.Number))
const iso = Schema.encodeSync(codec)
const back = Schema.decodeSync(codec)
// `iso`/`back` round-trip Option values through the Iso form losslessly

Overrides the derived ISO codec with an explicit target codec and a decode / encode getter pair. The resulting schema carries a custom Iso type parameter.

import { Schema, SchemaGetter } from "effect"
// Expose a Date as an ISO string on the Iso side
const MyDate = Schema.Date.pipe(
Schema.overrideToCodecIso(Schema.String, {
decode: SchemaGetter.transform((s: string) => new Date(s)),
encode: SchemaGetter.transform((d: Date) => d.toISOString())
})
)
Schema.encodeSync(Schema.toCodecIso(MyDate))(new Date("2024-01-01T00:00:00.000Z"))
// => "2024-01-01T00:00:00.000Z"

Derives a Codec<T, StringTree> — every leaf value becomes a string while the surrounding structure (objects, arrays) is preserved. This is the format behind URL query-string and form-data encoding. Declarations are converted to undefined unless they carry a toCodecJson / toCodec annotation.

import { Schema } from "effect"
const codec = Schema.toCodecStringTree(
Schema.Struct({ page: Schema.Number, q: Schema.String })
)
Schema.encodeSync(codec)({ page: 2, q: "effect" })
// => { page: "2", q: "effect" } // every leaf is a string
Schema.decodeSync(codec)({ page: "2", q: "effect" })
// => { page: 2, q: "effect" }

Pass { keepDeclarations: true } to preserve non-string declaration values (numbers, Blob, …) when they are compatible with the schema — this is what Schema.fromFormData uses to keep uploaded files intact:

import { Schema } from "effect"
const codec = Schema.toCodecStringTree(
Schema.Struct({ a: Schema.Int }),
{ keepDeclarations: true }
)

Derives an XML encoder from a codec. The returned function encodes a value through toCodecStringTree and yields an Effect that succeeds with an XML string (or fails with SchemaError if encoding fails). Options control the root element name, array item name, pretty-printing, indentation, and key sorting.

import { Effect, Schema } from "effect"
const toXml = Schema.toEncoderXml(
Schema.Struct({ name: Schema.String, age: Schema.Number }),
{ rootName: "person" }
)
Effect.runPromise(toXml({ name: "Alice", age: 30 })).then(console.log)
// => <person>
// => <age>30</age>
// => <name>Alice</name>
// => </person>

Derives a Differ<T, JsonPatch.JsonPatch> from a codec. It serializes values to JSON (via toCodecJson), computes RFC 6902 JSON Patch operations between an old and new value with diff, and can apply a patch back to the typed value with patch. patch returns the original reference when nothing changed.

import { Schema } from "effect"
const differ = Schema.toDifferJsonPatch(
Schema.Struct({ a: Schema.String, b: Schema.Number })
)
const patch = differ.diff({ a: "x", b: 1 }, { a: "x", b: 2 })
// => [{ op: "replace", path: "/b", value: 2 }]
differ.patch({ a: "x", b: 1 }, patch)
// => { a: "x", b: 2 }
differ.empty // => [] (no-op patch)
differ.combine(patch, differ.empty) // => patch (concatenated ops)

Produces a Standard Schema v1 object so the schema can be consumed by any Standard-Schema-compatible library (form libraries, validators, etc.). The returned object exposes a ~standard.validate method. Optional leafHook / checkHook customize issue messages, and parseOptions forwards parse settings (defaults to collecting all errors).

import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.NonEmptyString,
age: Schema.Number
})
const standard = Schema.toStandardSchemaV1(Person)
standard["~standard"].validate({ name: "Alice", age: 30 })
// => { value: { name: "Alice", age: 30 } }
standard["~standard"].validate({ name: "", age: 30 })
// => { issues: [{ path: ["name"], message: "..." }] }

Produces an (experimental) Standard JSON Schema v1 object. It attaches a ~standard.jsonSchema accessor with input / output methods that emit JSON Schema for the encoded and decoded sides, targeting either draft-2020-12 or draft-07.

import { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.String })
const standard = Schema.toStandardJSONSchemaV1(Person)
standard["~standard"].jsonSchema.input({ target: "draft-2020-12" })
// => { type: "object", properties: { name: { type: "string" } }, ... }

Returns a full JSON Schema document (draft 2020-12) with a schema and a definitions map. This is the primary JSON Schema entry point — see the dedicated JSON Schema page for options (additionalProperties, generateDescriptions, includeAnnotationKey) and a deeper walk-through.

import { Schema } from "effect"
const document = Schema.toJsonSchemaDocument(
Schema.Struct({ name: Schema.String })
)
document.dialect // => "draft-2020-12"
document.schema // => { type: "object", properties: { name: ... }, ... }
document.definitions // => {} (named $defs, if any)

These derive optics from a schema’s Iso boundary, letting you focus into the serialized form.

Derives an Iso<Type, Iso> optic that isomorphically converts between the schema’s Type and its Iso (intermediate / serialized) form, built on top of toCodecIso.

import { Schema } from "effect"
const iso = Schema.toIso(Schema.Option(Schema.Number))
// => Iso focusing the Iso representation of Option<number>

Returns the identity Iso<Type, Type> over the schema’s source (decoded) side — a convenient starting point when composing optics that stay on the Type side.

import { Schema } from "effect"
const iso = Schema.toIsoSource(Schema.String) // => Iso<string, string>

Returns the identity Iso<Iso, Iso> over the schema’s focus (encoded Iso) side — the counterpart to toIsoSource for composing on the Iso side.

import { Schema } from "effect"
const iso = Schema.toIsoFocus(Schema.String) // => Iso over the Iso side

Every structural derivation can be customized per-schema by attaching an annotation through the matching overrideTo* helper. The override is picked up the next time you call the corresponding to* function on that schema (or any schema that contains it).

DerivationOverride helper
toEquivalenceoverrideToEquivalence
toFormatteroverrideToFormatter
toCodecIsooverrideToCodecIso
import { Schema } from "effect"
// One schema, two overrides — both flow through the derivations below
const Temperature = Schema.Number.pipe(
Schema.overrideToFormatter(() => (c) => `${c}°C`),
Schema.overrideToEquivalence(() => (a, b) => Math.round(a) === Math.round(b))
)
Schema.toFormatter(Temperature)(21.4) // => "21.4°C"
Schema.toEquivalence(Temperature)(21.4, 21.0) // => true