1# Schema
2
3`Schema` is a TypeScript-first library for defining data shapes, validating unknown input, and transforming values between formats.
4
5Two key concepts appear throughout this guide:
6
7- **Decoding** — turning unknown external data (API responses, form submissions, config files) into typed, validated values.
8- **Encoding** — turning typed values back into a serializable format (JSON, FormData, etc.).
9
10Use Schema to:
11
12- **Define types** — declare the shape of your data once and get both the TypeScript type and a runtime validator.
13- **Validate input** — decode unknown data into type-safe values, with clear error messages when it doesn't match.
14- **Transform values** — convert between your domain types and serialization formats like JSON, FormData, and URLSearchParams.
15- **Generate tooling** — derive JSON Schemas, test data generators, equivalence checks, and more from a single schema definition.
16
17## Design Philosophy
18
19- **Lightweight by default** — only import the features you need, keeping your bundle small.
20- **Familiar API** — naming conventions and patterns are consistent with popular validation libraries, so getting started is easy.
21- **Explicit** — you choose which features to use. Nothing is included implicitly.
22
23### What's in This Guide
24
251. **Elementary schemas** — built-in schemas for primitives, literals, strings, numbers, dates, and template literals.
262. **Composite schemas** — combine elementary schemas into structs (objects), tuples, arrays, records, and unions.
273. **Validation** — add runtime checks (filters) to constrain values, report multiple errors, and define custom rules.
284. **Constructors** — create validated values at runtime, with support for defaults, brands, and refinements.
295. **Transformations** — convert values between types during decoding and encoding. Transformations are reusable objects you compose with schemas.
306. **Flipping** — swap a schema's decoding and encoding directions.
317. **Classes and opaque types** — create distinct TypeScript types backed by structs, with optional methods and equality.
328. **Serialization** — convert values to and from JSON, FormData, URLSearchParams, and XML using canonical codecs.
339. **Tooling** — generate JSON Schemas, test data generators (Arbitraries), equivalence checks, optics, and JSON Patch differs from a single schema.
3410. **Error handling** — format validation errors for display, with hooks for internationalization.
3511. **Middlewares** — intercept decoding/encoding to provide fallbacks or inject services.
3612. **Advanced topics** — internal type model and type hierarchy (for library authors).
3713. **Integrations** — working examples for TanStack Form and Elysia.
3814. **[Migration from v3](../../migration/schema.md)** — API mapping from Schema v3 to v4.
39
40## Runtime Performance
41
42Effect Schema is benchmarked against the public
43[`schema-benchmarks`](https://github.com/open-circle/schema-benchmarks) suite.
44It exercises a realistic product schema across validation, parsing, error
45reporting, schema creation, and codecs.
46
47The table below compares Effect Schema with the Valibot and Zod cases available
48in the same suite.
49
50Values are microseconds per operation and lower is better. Results vary between
51machines, so they are most useful for understanding relative costs. A dash
52means that the library does not provide that benchmark.
53
54| Scenario | Effect Schema | Valibot 1.5.0 | Zod 4.6.2 |
55| ------------------------------------- | ------------: | ------------: | --------: |
56| Create a schema | 60.1297 | 1.0926 | 83.7496 |
57| Validate valid data | 4.1473 | 3.5389 | — |
58| Validate invalid data | 0.2571 | 0.1823 | — |
59| Parse valid data and collect errors | 5.0261 | 3.5393 | 7.0073 |
60| Parse invalid data and collect errors | 7.4838 | 5.7206 | 19.3064 |
61| Parse valid data and stop early | 4.1559 | 3.5941 | — |
62| Parse invalid data and stop early | 0.2525 | 0.1917 | — |
63| Standard Schema, valid data | **5.3408** | 3.5565 | 3.5755 |
64| Standard Schema, invalid data | 11.7723 | 5.4422 | 15.9974 |
65| Encode with a typed codec | 0.0827 | — | 0.0441 |
66| Decode with a typed codec | 0.1063 | — | 0.0427 |
67
68## Experimental schema compilers
69
70Enable JIT compilation at application startup with a side-effect import:
71
72```ts
73import "effect/schema/SchemaJITCompiler/enable"
74```
75
76Alternatively, `SchemaJITCompiler.enable(schema.ast)` enables one AST and the
77dependencies reached while parsing it. Importing `SchemaJITCompiler` or the
78`schema` barrel alone does not enable compilation. Operations are
79prepared on first use. If dynamic function construction is blocked or compilation
80fails, the interpreter remains available. Exceptions from executing a parser are
81not treated as compilation failures and do not trigger a retry.
82
83JIT and AOT use the same source generator. To generate an AOT module at build
84time, call `SchemaAOTCompiler.compile(targets)` with an ordered array of ASTs
85and the operations to prepare:
86
87```ts
88SchemaAOTCompiler.compile([
89 { ast: User.ast, operations: ["decode"] },
90 { ast: SchemaAST.toType(User.ast), operations: ["is", "make"] }
91])
92```
93
94The module exports `install(asts)`. Call it with the target ASTs in the same
95order before using normal `SchemaParser` functions. Generated modules contain
96only the requested operation families and their dependencies. Operations that
97were not requested use the interpreter if they are called. Generated modules
98do not import the generator and work where `new Function` is forbidden.
99
100The low-level installation trusts the supplied root order and AST definitions.
101Target `SchemaAST.toType(schema.ast)` with `is` or `make` for guards and
102construction. Target `SchemaAST.flip(schema.ast)` with `decode` for encoding.
103
104`effect/schema/SchemaAOTCompiler/Build` provides the higher-level
105workflow. Its `build` function loads direct Schema exports, writes a
106self-installing module through `FileSystem`, and prepares decoding by default:
107
108```ts
109import * as SchemaAOTCompilerBuild from "effect/schema/SchemaAOTCompiler/Build"
110
111SchemaAOTCompilerBuild.build({
112 modules: {
113 "./schemas/User.js": () => import("./schemas/User.js"),
114 "./schemas/Order.js": () => import("./schemas/Order.js")
115 },
116 baseUrl: import.meta.url,
117 outFile: "./generated/schema-aot.js"
118})
119```
120
121Import the generated file at application startup. Module keys identify imports
122relative to `baseUrl`; each loader must return that same module during the
123build. The lazy record produced by `import.meta.glob` can be passed directly.
124Request `encode`, `is`, or `make` explicitly when those directions also need
125AOT roots. Loading executes the selected application modules during the build.
126Run the returned Effect with the platform's `FileSystem` and `Path` services,
127and ensure the bundler retains the generated side-effect import.
128
129Regenerate AOT modules when schema definitions or the Effect version change.
130Callbacks and symbols are read from runtime ASTs, not serialized.
131
132### One registry for all implementations
133
134A single `WeakMap` associates each exact AST with its decoder entry. The cache
135stores functions, never parsing results. The interpreter, JIT, AOT and
136`SchemaCompiler.set(ast, decoder)` all use it.
137
138| Operation | Result | Purpose |
139| -------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------- |
140| `decodeEffect` | `Effect` with output or detailed issues | Required complete decoding, including asynchronous work and transformations. |
141| `decode` | Output or `SchemaCompiler.invalid` | Optional synchronous decoding fast path without detailed diagnostics. |
142| `is` | Boolean | Optional validation without constructing output. |
143| `make` | Output or `SchemaCompiler.invalid` | Optional synchronous construction fast path without detailed diagnostics. |
144| `makeEffect` | `Effect` with a constructed value or issues | Optional specialized construction. The registry caches the interpreted constructor when absent. |
145
146Decoding tries `decode` when available. Success provides the output directly;
147failure calls `decodeEffect` for diagnostics. The diagnostic traversal uses child
148decoders directly, without restarting their validation fast paths. A boolean
149guard prefers `is`, otherwise it uses ordinary decoding (including `decode`
150when available). An `invalid` result needs that diagnostic fallback because the
151marker is also a possible input value. Composite checks
152can require stripped, reconstructed values, so `is` is omitted when it cannot
153avoid constructing those values safely.
154
155Each operation initializes independently. Synchronous construction tries `make`
156when available and falls back to `makeEffect` for detailed issues. Compilers omit
157`make` whenever replay could repeat defaults, Class constructors,
158transformations, middleware, or other effects. `makeEffect` itself never uses
159validation replay. Field defaults belong to the parent occurrence, not to
160construction of the root. Runtime parse options, including product concurrency,
161retain the interpreter's semantics.
162
163Installing a decoder replaces the entry for that AST. Existing consumers that
164already captured an entry retain it. Late installation is allowed, but startup
165installation is needed to optimize every consumer. Custom decoders supplied to
166`set` are trusted to implement the AST's semantics.
167
168An AOT module installs entries for explicitly requested roots and their static
169dependencies. Each generated operation initializes on first use. All entries
170live in the same `WeakMap`: there is no separate AOT cache, and a later
171`SchemaCompiler.set` for the same AST replaces any compiled entry in the same way.
172
173### What is specialized
174
175Encoding-free graphs of supported primitives, Objects, Arrays, tuples, Unions
176and template literals can use generated validators. Struct and homogeneous Array
177decoding and construction also have generated loops. Pure fixed Struct and
178homogeneous Array constructors can additionally use the synchronous `make` fast
179path; composite children are resolved through the same registry. These loops
180share the interpreter's diagnostic and asynchronous continuation helpers.
181Other detailed traversals and constructors use the existing interpreter with
182registry-resolved children; there is no separate diagnostic interpreter in the
183compiler.
184
185Transformations and middleware never participate in validation replay. A single
186synchronous transformation between supported leaf types can use generated
187orchestration directly. Other transformations and middleware use the interpreted
188orchestration, while pure child checkpoints can still use generated validators.
189Suspend is resolved lazily by JIT. AOT does not evaluate Suspend thunks at build
190time, so dynamically reached schemas fall back to the interpreter unless installed
191separately. Declaration callbacks remain runtime code; their type parameters can
192be compiled.
193
194Checks and property getters in replayable validation must be deterministic and
195free of side effects. Proxy inputs and modifications to built-in object behavior
196are not supported by the optimization contract. Large or unsupported graphs
197retain interpreted paths. AOT removes dynamic source generation, not all parser
198initialization or the need for runtime schema objects.
199
200# Defining Elementary Schemas
201
202Schema provides built-in schemas for all common TypeScript types. These schemas represent a single value — like a string or a number — and they are the building blocks you combine into more complex shapes.
203
204## Primitives
205
206Use these schemas when a value should be exactly one of the basic JavaScript types.
207
208```ts
209import { Schema } from "effect"
210
211// primitive types
212Schema.String
213Schema.Number
214Schema.BigInt
215Schema.Boolean
216Schema.Symbol
217Schema.Undefined
218Schema.Null
219```
220
221Sometimes you receive data that is not the right type yet — for example, a number that should become a string. You can build a schema that converts (coerces) values to the target type during decoding:
222
223```ts
224import { Getter, Parser, Schema } from "effect/schema"
225
226// ┌─── Codec<string, unknown>
227// ▼
228const schema = Schema.Unknown.pipe(
229 Schema.decodeTo(Schema.String, {
230 decode: Getter.String(),
231 encode: Getter.passthrough()
232 })
233)
234
235const parser = Parser.decodeUnknownSync(schema)
236
237console.log(parser("tuna")) // => "tuna"
238console.log(parser(42)) // => "42"
239console.log(parser(true)) // => "true"
240console.log(parser(null)) // => "null"
241```
242
243## Literals
244
245A literal schema matches one exact value. Use it when a field must be a specific string, number, or other constant.
246
247```ts
248import { Schema } from "effect"
249
250const tuna = Schema.Literal("tuna")
251const twelve = Schema.Literal(12)
252const twobig = Schema.Literal(2n)
253const tru = Schema.Literal(true)
254```
255
256Symbol literals:
257
258```ts
259import { Schema } from "effect"
260
261const terrific = Schema.UniqueSymbol(Symbol("terrific"))
262```
263
264`null`, `undefined`, and `void`:
265
266```ts
267import { Schema } from "effect"
268
269Schema.Null
270Schema.Undefined
271Schema.Void
272```
273
274To allow multiple literal values:
275
276```ts
277import { Schema } from "effect"
278
279const schema = Schema.Literals(["red", "green", "blue"])
280```
281
282To extract the set of allowed values from a literal schema:
283
284```ts
285import { Schema } from "effect"
286
287const schema = Schema.Literals(["red", "green", "blue"])
288
289// readonly ["red", "green", "blue"]
290schema.literals
291
292// readonly [Schema.Literal<"red">, Schema.Literal<"green">, Schema.Literal<"blue">]
293schema.members
294```
295
296## Strings
297
298You can add validation rules to a string schema. Each rule is applied with `.check(...)` and returns a new schema that enforces that constraint.
299
300```ts
301import { Schema } from "effect"
302
303Schema.String.check(Schema.isMaxLength(5))
304Schema.String.check(Schema.isMinLength(5))
305Schema.String.check(Schema.isBetweenLength(5, 5))
306Schema.String.check(Schema.isPattern(/^[a-z]+$/))
307Schema.String.check(Schema.isStartingWith("aaa"))
308Schema.String.check(Schema.isEndingWith("zzz"))
309Schema.String.check(Schema.isIncluding("---"))
310Schema.String.check(Schema.isUppercased())
311Schema.String.check(Schema.isLowercased())
312```
313
314To perform some simple string transforms:
315
316```ts
317import { Schema, SchemaTransformation } from "effect"
318
319Schema.String.decode(SchemaTransformation.trim())
320Schema.String.decode(SchemaTransformation.toLowerCase())
321Schema.String.decode(SchemaTransformation.toUpperCase())
322```
323
324## String formats
325
326Schema includes built-in checks for common string formats.
327
328```ts
329import { Schema } from "effect"
330
331Schema.String.check(Schema.isUUID())
332Schema.String.check(Schema.isBase64())
333Schema.String.check(Schema.isBase64Url())
334```
335
336## Numbers
337
338```ts
339import { Schema } from "effect"
340
341Schema.Number // all numbers
342Schema.Finite // finite numbers (i.e. not +/-Infinity or NaN)
343```
344
345You can add validation rules to a number schema. Each rule constrains the allowed range or value.
346
347```ts
348import { Schema } from "effect"
349
350Schema.Number.check(Schema.isBetween({ minimum: 5, maximum: 10 }))
351Schema.Number.check(Schema.isGreaterThan(5))
352Schema.Number.check(Schema.isGreaterThanOrEqualTo(5))
353Schema.Number.check(Schema.isLessThan(5))
354Schema.Number.check(Schema.isLessThanOrEqualTo(5))
355Schema.Number.check(Schema.isMultipleOf(5))
356```
357
358## Integers
359
360To require that a number has no decimal part, use `isInt()`. For 32-bit integers specifically, use `isInt32()`.
361
362```ts
363import { Schema } from "effect"
364
365Schema.Number.check(Schema.isInt())
366Schema.Number.check(Schema.isInt32())
367```
368
369## BigInts
370
371Schema does not ship pre-built BigInt validation factories (unlike numbers). Instead, you create your own using helper functions and a BigInt-compatible ordering. The example below shows how.
372
373```ts
374import { BigInt, Order, Schema } from "effect"
375
376const options = { order: Order.BigInt }
377
378const isBetween = Schema.makeIsBetween(options)
379const isGreaterThan = Schema.makeIsGreaterThan(options)
380const isGreaterThanOrEqualTo = Schema.makeIsGreaterThanOrEqualTo(options)
381const isLessThan = Schema.makeIsLessThan(options)
382const isLessThanOrEqualTo = Schema.makeIsLessThanOrEqualTo(options)
383const isMultipleOf = Schema.makeIsMultipleOf({
384 remainder: BigInt.remainder,
385 zero: 0n
386})
387
388const isPositive = isGreaterThan(0n)
389const isNonNegative = isGreaterThanOrEqualTo(0n)
390const isNegative = isLessThan(0n)
391const isNonPositive = isLessThanOrEqualTo(0n)
392
393Schema.BigInt.check(isBetween({ minimum: 5n, maximum: 10n }))
394Schema.BigInt.check(isGreaterThan(5n))
395Schema.BigInt.check(isGreaterThanOrEqualTo(5n))
396Schema.BigInt.check(isLessThan(5n))
397Schema.BigInt.check(isLessThanOrEqualTo(5n))
398Schema.BigInt.check(isMultipleOf(5n))
399Schema.BigInt.check(isPositive)
400Schema.BigInt.check(isNonNegative)
401Schema.BigInt.check(isNegative)
402Schema.BigInt.check(isNonPositive)
403```
404
405## Dates
406
407The `Schema.Date` schema matches valid `Date` objects and rejects invalid dates
408such as `new Date(NaN)`.
409
410## Template literals
411
412You can use `Schema.TemplateLiteral` to define structured string patterns made of multiple parts. Each part can be a literal or a schema, and **additional constraints** (such as `isMinLength` or `isMaxLength`) can be applied to individual parts.
413
414Template literal matching is based on the semantics of each part rather than only a generated regular expression. Checks on string, number, and bigint schema parts are applied while matching each segment.
415
416Parts must not contain encodings. Construction throws for transformed parts, including transformations inside unions and transformations whose decoded and encoded types are equal. Brands and supported checks without encodings remain valid. Use `Schema.TemplateLiteralParser` when the parts need to decode values, such as `BooleanFromBit` or `FiniteFromString`.
417
418To describe bit spellings directly, use `Schema.Literals([0, 1])` as the part. To describe finite numeric spellings, use `Schema.Finite`. Replacing `FiniteFromString` with `Finite` changes the accepted spelling rules: a finite numeric part does not accept an empty segment. Explicit `Schema.toType` and `Schema.toEncoded` projections remove transformations, but can also change the constraints a template validates.
419
420**Example** (Constraining parts of an email-like string)
421
422```ts
423import { Schema } from "effect"
424
425// Construct a template literal schema for values like `${string}@${string}`
426// Apply constraints to both sides of the "@" symbol
427const email = Schema.TemplateLiteral([
428 // Left part: must be a non-empty string
429 Schema.String.check(Schema.isMinLength(1)),
430
431 // Separator
432 "@",
433
434 // Right part: must be a string with a maximum length of 64
435 Schema.String.check(Schema.isMaxLength(64))
436])
437
438// The inferred type is `${string}@${string}`
439export type Type = typeof email.Type
440
441console.log(String(Schema.decodeUnknownExit(email)("a@b.com")))
442/*
443Success("a@b.com")
444*/
445
446console.log(String(Schema.decodeUnknownExit(email)("@b.com")))
447/*
448Failure(Cause([Fail(SchemaError(Expected a string matching template literal parts))]))
449*/
450```
451
452### Template literal parser
453
454If you want to extract the parts of a string that match a template, you can use `Schema.TemplateLiteralParser`. This allows you to parse the input into its individual components rather than treat it as a single string.
455
456The parser transforms a template built from the encoded sides of the parts into a tuple that retains their decoders and checks. Encoding applies the parts' encoders and joins the segments. The parser requires the decoding and encoding services of its parts in the corresponding direction.
457
458`Schema.toEncoded(parser)` validates that source template. Use `Schema.String` if you need to accept unrestricted strings.
459
460Ambiguous templates use greedy segmentation with backtracking. Encoding a tuple and decoding the resulting string can produce a different tuple when a segment contains a separator used by the template.
461
462**Example** (Parsing a template literal into components)
463
464```ts
465import { Schema } from "effect"
466
467const schema = Schema.TemplateLiteralParser([
468 Schema.String.check(Schema.isMinLength(2)),
469 ":",
470 Schema.Int
471])
472
473// The inferred type is `readonly [string, ":", number]`
474export type Type = typeof schema.Type
475
476console.log(String(Schema.decodeUnknownExit(schema)("aa:1")))
477// Success(["aa",":",1])
478
479console.log(String(Schema.decodeUnknownExit(schema)("a:1")))
480// Failure(Cause([Fail(SchemaError(Expected a value with a length of at least 2
481// at [0]))]))
482
483console.log(String(Schema.decodeUnknownExit(schema)("aa:1.2")))
484// Failure(Cause([Fail(SchemaError(Expected an integer
485// at [2]))]))
486```
487
488# Defining Composite Schemas
489
490Once you have elementary schemas, you can combine them into composite schemas that describe objects, arrays, tuples, key-value maps, and unions.
491
492## Structs
493
494A struct schema describes a JavaScript object with a known set of keys. Each key maps to a schema that validates and types its value.
495
496### Optional and Mutable Keys
497
498By default, every key in a struct is required and readonly. Use `Schema.optionalKey` to make a key optional (the key can be absent from the object), and `Schema.mutableKey` to make it writable.
499
500You can mark struct properties as optional or mutable using `Schema.optionalKey` and `Schema.mutableKey`.
501
502```ts
503import { Schema } from "effect"
504
505const schema = Schema.Struct({
506 a: Schema.String,
507 b: Schema.optionalKey(Schema.String),
508 c: Schema.mutableKey(Schema.String),
509 d: Schema.optionalKey(Schema.mutableKey(Schema.String))
510})
511
512/*
513with "exactOptionalPropertyTypes": true
514
515type Type = {
516 readonly a: string;
517 readonly b?: string;
518 c: string;
519 d?: string;
520}
521*/
522type Type = (typeof schema)["Type"]
523```
524
525### Optional Fields
526
527There are several ways to represent optional properties, depending on whether you want `undefined` in the type, `null` in the type, or just a missing key. By combining `Schema.optionalKey`, `Schema.optional`, and `Schema.NullOr`, you can represent any variant.
528
529```ts
530import { Schema } from "effect"
531
532export const schema = Schema.Struct({
533 // Exact Optional Property
534 a: Schema.optionalKey(Schema.FiniteFromString),
535 // Optional Property
536 b: Schema.optional(Schema.FiniteFromString),
537 // Exact Optional Property with Nullability
538 c: Schema.optionalKey(Schema.NullOr(Schema.FiniteFromString)),
539 // Optional Property with Nullability
540 d: Schema.optional(Schema.NullOr(Schema.FiniteFromString))
541})
542
543/*
544type Encoded = {
545 readonly a?: string;
546 readonly b?: string | undefined;
547 readonly c?: string | null;
548 readonly d?: string | null | undefined;
549}
550*/
551type Encoded = typeof schema.Encoded
552
553/*
554type Type = {
555 readonly a?: number;
556 readonly b?: number | undefined;
557 readonly c?: number | null;
558 readonly d?: number | null | undefined;
559}
560*/
561type Type = typeof schema.Type
562```
563
564#### Omitting Values When Transforming Optional Fields
565
566If an optional field arrives as `undefined`, you may want to omit it from the output entirely rather than keeping it.
567
568```ts
569import { Option, Predicate, Schema, SchemaGetter } from "effect"
570
571export const schema = Schema.Struct({
572 a: Schema.optional(Schema.FiniteFromString).pipe(
573 Schema.decodeTo(Schema.optionalKey(Schema.Number), {
574 decode: SchemaGetter.transformOptional(
575 Option.filter(Predicate.isNotUndefined) // omit undefined
576 ),
577 encode: SchemaGetter.passthrough()
578 })
579 )
580})
581
582/*
583type Encoded = {
584 readonly a?: string | undefined;
585}
586*/
587type Encoded = typeof schema.Encoded
588
589/*
590type Type = {
591 readonly a?: number;
592}
593*/
594type Type = typeof schema.Type
595```
596
597#### Representing Optional Fields with never Type
598
599You can use `Schema.Never` inside an optional key to represent a field that should never have a value but may still appear as a key in the type.
600
601```ts
602import { Schema } from "effect"
603
604export const schema = Schema.Struct({
605 a: Schema.optionalKey(Schema.Never)
606})
607
608/*
609type Encoded = {
610 readonly a?: never;
611}
612*/
613type Encoded = typeof schema.Encoded
614
615/*
616type Type = {
617 readonly a?: never;
618}
619*/
620type Type = typeof schema.Type
621```
622
623### Decoding Defaults
624
625You can assign default values to fields during decoding using:
626
627| API | Encoded side | Default value type |
628| ----------------------------------- | ------------------------- | ------------------ |
629| `Schema.withDecodingDefaultKey` | key absent | `Encoded` |
630| `Schema.withDecodingDefault` | key absent or `undefined` | `Encoded` |
631| `Schema.withDecodingDefaultTypeKey` | key absent | `Type` |
632| `Schema.withDecodingDefaultType` | key absent or `undefined` | `Type` |
633
634The "Key" variants use `optionalKey` (the key may be absent but not `undefined`), while the non-"Key" variants use `optional` (the key may be absent **or** `undefined`).
635
636The "Type" variants accept a default specified as a `Type` (decoded) value, which is useful when the schema has a transformation and you want to provide the default in the decoded representation.
637
638#### Encoded-Side Defaults
639
640`withDecodingDefaultKey` and `withDecodingDefault` accept a default specified as an
641**`Encoded` value** (before any decoding transformation). This is the most common
642case and works well when the Encoded and Type representations are the same, or
643when you already have the value in encoded form.
644
645**Example** (Default as an Encoded value)
646
647In `FiniteFromString`, the `Encoded` type is `string` and the `Type` is `number`.
648The default `"1"` is a **string** (the Encoded type), which is then decoded to `1`.
649
650```ts
651import { Effect, Schema } from "effect"
652
653const schema = Schema.Struct({
654 // ┌─── "1" is a string (Encoded type)
655 // ▼
656 a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1")))
657})
658
659// ┌─── { readonly a?: string | undefined; }
660// ▼
661type Encoded = typeof schema.Encoded
662
663// ┌─── { readonly a: number; }
664// ▼
665type Type = typeof schema.Type
666
667console.log(Schema.decodeUnknownSync(schema)({}))
668// Output: { a: 1 }
669
670console.log(Schema.decodeUnknownSync(schema)({ a: undefined }))
671// Output: { a: 1 }
672
673console.log(Schema.decodeUnknownSync(schema)({ a: "2" }))
674// Output: { a: 2 }
675```
676
677#### Type-Side Defaults
678
679`withDecodingDefaultTypeKey` and `withDecodingDefaultType` accept a default
680specified as a **`Type` value** (the decoded representation). This is useful when
681the schema has a transformation and you want to provide the default directly as a
682decoded value, bypassing the decoding step.
683
684**Example** (Default as a Type value)
685
686Here the default `1` is a **number** (the Type), not a string. It does not go
687through the `FiniteFromString` decoding transformation.
688
689```ts
690import { Effect, Schema } from "effect"
691
692const schema = Schema.Struct({
693 // ┌─── 1 is a number (Type)
694 // ▼
695 a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(Effect.succeed(1)))
696})
697
698// ┌─── { readonly a?: string | undefined; }
699// ▼
700type Encoded = typeof schema.Encoded
701
702// ┌─── { readonly a: number; }
703// ▼
704type Type = typeof schema.Type
705
706console.log(Schema.decodeUnknownSync(schema)({}))
707// Output: { a: 1 }
708
709console.log(Schema.decodeUnknownSync(schema)({ a: undefined }))
710// Output: { a: 1 }
711
712console.log(Schema.decodeUnknownSync(schema)({ a: "2" }))
713// Output: { a: 2 }
714```
715
716#### Nested Decoding Defaults
717
718You can also apply decoding defaults within nested structures.
719
720**Example** (Nested struct with defaults for missing or undefined fields)
721
722```ts
723import { Effect, Schema } from "effect"
724
725const schema = Schema.Struct({
726 a: Schema.Struct({
727 b: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1")))
728 }).pipe(Schema.withDecodingDefault(Effect.succeed({})))
729})
730
731/*
732type Encoded = {
733 readonly a?: {
734 readonly b?: string | undefined;
735 } | undefined;
736}
737*/
738type Encoded = typeof schema.Encoded
739
740/*
741type Type = {
742 readonly a: {
743 readonly b: number;
744 };
745}
746*/
747type Type = typeof schema.Type
748
749console.log(Schema.decodeUnknownSync(schema)({}))
750// Output: { a: { b: 1 } }
751
752console.log(Schema.decodeUnknownSync(schema)({ a: undefined }))
753// Output: { a: { b: 1 } }
754
755console.log(Schema.decodeUnknownSync(schema)({ a: {} }))
756// Output: { a: { b: 1 } }
757
758console.log(Schema.decodeUnknownSync(schema)({ a: { b: undefined } }))
759// Output: { a: { b: 1 } }
760
761console.log(Schema.decodeUnknownSync(schema)({ a: { b: "2" } }))
762// Output: { a: { b: 2 } }
763```
764
765### Manual Decoding Defaults
766
767If the defaulting logic is more specific than just handling `undefined` or missing values, you can use `Schema.decodeTo` to apply custom fallback rules.
768
769This is useful when you need to account for values like `null` or other invalid states.
770
771**Example** (Providing a fallback when value is `null` or missing)
772
773```ts
774import { Option, Predicate, Schema, SchemaGetter } from "effect"
775
776const schema = Schema.Struct({
777 a: Schema.optionalKey(Schema.NullOr(Schema.String)).pipe(
778 Schema.decodeTo(Schema.FiniteFromString, {
779 decode: SchemaGetter.transformOptional((oe) =>
780 oe.pipe(
781 // remove null values
782 Option.filter(Predicate.isNotNull),
783 // default to "1" if none
784 Option.orElseSome(() => "1")
785 )
786 ),
787 encode: SchemaGetter.passthrough()
788 })
789 )
790})
791
792// ┌─── { readonly a?: string | null; }
793// ▼
794type Encoded = typeof schema.Encoded
795
796// ┌─── { readonly a: number; }
797// ▼
798type Type = typeof schema.Type
799
800console.log(Schema.decodeUnknownSync(schema)({}))
801// Output: { a: 1 }
802
803// console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
804// throws
805
806console.log(Schema.decodeUnknownSync(schema)({ a: null }))
807// Output: { a: 1 }
808
809console.log(Schema.decodeUnknownSync(schema)({ a: "2" }))
810// Output: { a: 2 }
811```
812
813**Example** (Providing a fallback when value is `null`, `undefined`, or missing)
814
815```ts
816import { Option, Predicate, Schema, SchemaGetter } from "effect"
817
818const schema = Schema.Struct({
819 a: Schema.optional(Schema.NullOr(Schema.String)).pipe(
820 Schema.decodeTo(Schema.FiniteFromString, {
821 decode: SchemaGetter.transformOptional((oe) =>
822 oe.pipe(
823 // remove null and undefined
824 Option.filter(Predicate.isNotNullish),
825 // default to "1" if none
826 Option.orElseSome(() => "1")
827 )
828 ),
829 encode: SchemaGetter.passthrough()
830 })
831 )
832})
833
834// ┌─── { readonly a?: string | null | undefined; }
835// ▼
836type Encoded = typeof schema.Encoded
837
838// ┌─── { readonly a: number; }
839// ▼
840type Type = typeof schema.Type
841
842console.log(Schema.decodeUnknownSync(schema)({}))
843// Output: { a: 1 }
844
845console.log(Schema.decodeUnknownSync(schema)({ a: undefined }))
846// Output: { a: 1 }
847
848console.log(Schema.decodeUnknownSync(schema)({ a: null }))
849// Output: { a: 1 }
850
851console.log(Schema.decodeUnknownSync(schema)({ a: "2" }))
852// Output: { a: 2 }
853```
854
855### Optional Fields as Options
856
857Effect's `Option` type is a safer alternative to `undefined` for representing the presence or absence of a value. These helpers convert between optional struct fields and `Option` values.
858
859#### Exact Optional Property
860
861```ts
862import { Option, Schema } from "effect"
863
864const Product = Schema.Struct({
865 quantity: Schema.OptionFromOptionalKey(Schema.FiniteFromString)
866})
867
868// ┌─── { readonly quantity?: string; }
869// ▼
870type Encoded = typeof Product.Encoded
871
872// ┌─── { readonly quantity: Option<number>; }
873// ▼
874type Type = typeof Product.Type
875
876console.log(Schema.decodeUnknownSync(Product)({}))
877// Output: { quantity: { _id: 'Option', _tag: 'None' } }
878
879console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
880// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } }
881
882// console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
883// throws
884
885console.log(Schema.encodeSync(Product)({ quantity: Option.some(2) }))
886// Output: { quantity: "2" }
887
888console.log(Schema.encodeSync(Product)({ quantity: Option.none() }))
889// Output: {}
890```
891
892#### Optional Property
893
894```ts
895import { Option, Schema } from "effect"
896
897const Product = Schema.Struct({
898 quantity: Schema.OptionFromOptional(Schema.FiniteFromString)
899})
900
901// ┌─── { readonly quantity?: string | undefined; }
902// ▼
903type Encoded = typeof Product.Encoded
904
905// ┌─── { readonly quantity: Option<number>; }
906// ▼
907type Type = typeof Product.Type
908
909console.log(Schema.decodeUnknownSync(Product)({}))
910// Output: { quantity: { _id: 'Option', _tag: 'None' } }
911
912console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
913// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } }
914
915console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
916// Output: { quantity: { _id: 'Option', _tag: 'None' } }
917
918console.log(Schema.encodeSync(Product)({ quantity: Option.some(2) }))
919// Output: { quantity: "2" }
920
921console.log(Schema.encodeSync(Product)({ quantity: Option.none() }))
922// Output: {}
923```
924
925#### Exact Optional Property with Nullability
926
927```ts
928import { Option, Predicate, Schema, SchemaTransformation } from "effect"
929
930const Product = Schema.Struct({
931 quantity: Schema.optionalKey(Schema.NullOr(Schema.FiniteFromString)).pipe(
932 Schema.decodeTo(
933 Schema.Option(Schema.Number),
934 SchemaTransformation.transformOptional({
935 decode: (oe) => oe.pipe(Option.filter(Predicate.isNotNull), Option.some),
936 encode: Option.flatten
937 })
938 )
939 )
940})
941
942// ┌─── { readonly quantity?: string | null; }
943// ▼
944type Encoded = typeof Product.Encoded
945
946// ┌─── { readonly quantity: Option<number>; }
947// ▼
948type Type = typeof Product.Type
949
950console.log(Schema.decodeUnknownSync(Product)({}))
951// Output: { quantity: { _id: 'Option', _tag: 'None' } }
952
953console.log(Schema.decodeUnknownSync(Product)({ quantity: null }))
954// Output: { quantity: { _id: 'Option', _tag: 'None' } }
955
956console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
957// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } }
958
959// console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
960// throws
961```
962
963#### Optional Property with Nullability
964
965```ts
966import { Schema } from "effect"
967
968const Product = Schema.Struct({
969 quantity: Schema.OptionFromOptionalNullOr(Schema.FiniteFromString)
970})
971
972// ┌─── { readonly quantity?: string | null | undefined; }
973// ▼
974type Encoded = typeof Product.Encoded
975
976// ┌─── { readonly quantity: Option<number>; }
977// ▼
978type Type = typeof Product.Type
979
980console.log(Schema.decodeUnknownSync(Product)({}))
981// Output: { quantity: { _id: 'Option', _tag: 'None' } }
982
983console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
984// Output: { quantity: { _id: 'Option', _tag: 'None' } }
985
986console.log(Schema.decodeUnknownSync(Product)({ quantity: null }))
987// Output: { quantity: { _id: 'Option', _tag: 'None' } }
988
989console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
990// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 }
991```
992
993### Key Annotations
994
995You can annotate individual keys using the `annotateKey` method. This is useful for adding a description or customizing the error message shown when the key is missing.
996
997**Example** (Annotating a required `username` field)
998
999```ts
1000import { Schema } from "effect"
1001
1002const schema = Schema.Struct({
1003 username: Schema.String.annotateKey({
1004 description: "The username used to log in",
1005 // Custom message shown if the key is missing
1006 messageMissingKey: "Username is required"
1007 })
1008})
1009
1010console.log(String(Schema.decodeUnknownExit(schema)({})))
1011/*
1012Failure(Cause([Fail(SchemaError: Username is required
1013 at ["username"]
1014)]))
1015*/
1016```
1017
1018### Unexpected Key Message
1019
1020You can annotate a struct with a custom message to use when a key is unexpected (when `onExcessProperty` is `error`).
1021
1022**Example** (Annotating a struct with a custom message)
1023
1024```ts
1025import { Schema } from "effect"
1026
1027const schema = Schema.Struct({
1028 a: Schema.String
1029}).annotate({ messageUnexpectedKey: "Custom message" })
1030
1031console.log(String(Schema.decodeUnknownExit(schema)({ a: "a", b: "b" }, { onExcessProperty: "error" })))
1032/*
1033Failure(Cause([Fail(SchemaError: Custom message
1034 at ["b"]
1035)]))
1036*/
1037```
1038
1039### Handling unexpected keys
1040
1041Unexpected keys are ignored by default. Set `onExcessProperty` to `error` to
1042reject them. To retain additional keys, describe and validate them with
1043`Schema.Record` or `Schema.StructWithRest` so they are represented in the
1044schema's type.
1045
1046### Index Signatures
1047
1048An index signature lets a struct accept any string key in addition to its fixed keys. Use `Schema.StructWithRest` to combine a struct with one or more record schemas.
1049
1050Filters applied to either the struct or the record are preserved when combined.
1051
1052**Example** (Combining fixed properties with an index signature)
1053
1054```ts
1055import { Schema } from "effect"
1056
1057// Define a schema with one fixed key "a" and any number of string keys mapping to numbers
1058export const schema = Schema.StructWithRest(Schema.Struct({ a: Schema.Number }), [
1059 Schema.Record(Schema.String, Schema.Number)
1060])
1061
1062/*
1063type Type = {
1064 readonly [x: string]: number;
1065 readonly a: number;
1066}
1067*/
1068type Type = typeof schema.Type
1069
1070/*
1071type Encoded = {
1072 readonly [x: string]: number;
1073 readonly a: number;
1074}
1075*/
1076type Encoded = typeof schema.Encoded
1077```
1078
1079If you want the record part to be mutable, apply `Schema.mutableKey` to its value schema.
1080
1081**Example** (Allowing dynamic keys to be mutable)
1082
1083```ts
1084import { Schema } from "effect"
1085
1086// Define a schema with one fixed key "a" and any number of string keys mapping to numbers
1087export const schema = Schema.StructWithRest(Schema.Struct({ a: Schema.Number }), [
1088 Schema.Record(Schema.String, Schema.mutableKey(Schema.Number))
1089])
1090
1091/*
1092type Type = {
1093 [x: string]: number;
1094 readonly a: number;
1095}
1096*/
1097type Type = typeof schema.Type
1098
1099/*
1100type Encoded = {
1101 [x: string]: number;
1102 readonly a: number;
1103}
1104*/
1105type Encoded = typeof schema.Encoded
1106```
1107
1108### Renaming Encoded Keys
1109
1110Use `Schema.encodeKeys` to rename one or more keys only in the encoded representation of a struct.
1111
1112Pass a mapping of `{ decodedKey: encodedKey }`. During decoding, the schema expects the mapped encoded keys. During encoding, it produces those keys. Keys not in the mapping are left unchanged.
1113
1114Unlike `Struct.renameKeys`, this does not rename the struct's own field names. It only remaps keys at the encoding / decoding boundary.
1115
1116**Example** (Using snake_case keys in the encoded form)
1117
1118```ts
1119import { Schema } from "effect"
1120
1121const schema = Schema.Struct({
1122 userId: Schema.FiniteFromString,
1123 accountName: Schema.String
1124}).pipe(
1125 Schema.encodeKeys({
1126 userId: "user_id",
1127 accountName: "account_name"
1128 })
1129)
1130
1131console.log(Schema.decodeUnknownSync(schema)({ user_id: "1", account_name: "alice" }))
1132// { userId: 1, accountName: "alice" }
1133
1134console.log(Schema.encodeUnknownSync(schema)({ userId: 1, accountName: "alice" }))
1135// { user_id: "1", account_name: "alice" }
1136```
1137
1138If you are building a struct from reused fields or `Schema.fieldsAssign`, apply `Schema.encodeKeys` after defining the full struct.
1139
1140### Reusing Fields
1141
1142Every `Schema.Struct` exposes a `.fields` property containing its field definitions. You can spread these fields into a new struct to reuse them, similar to how TypeScript interfaces use `extends`.
1143
1144**Example** (Single inheritance)
1145
1146```ts
1147import { Schema } from "effect"
1148
1149const Timestamped = Schema.Struct({
1150 createdAt: Schema.Date,
1151 updatedAt: Schema.Date
1152})
1153
1154const User = Schema.Struct({
1155 ...Timestamped.fields,
1156 name: Schema.String,
1157 email: Schema.String
1158})
1159
1160const Post = Schema.Struct({
1161 ...Timestamped.fields,
1162 title: Schema.String,
1163 body: Schema.String
1164})
1165```
1166
1167**Example** (Multiple inheritance)
1168
1169```ts
1170import { Schema } from "effect"
1171
1172const Timestamped = Schema.Struct({
1173 createdAt: Schema.Date,
1174 updatedAt: Schema.Date
1175})
1176
1177const SoftDeletable = Schema.Struct({
1178 deletedAt: Schema.optionalKey(Schema.Date)
1179})
1180
1181const User = Schema.Struct({
1182 ...Timestamped.fields,
1183 ...SoftDeletable.fields,
1184 name: Schema.String,
1185 email: Schema.String
1186})
1187```
1188
1189### Deriving Structs
1190
1191You can derive new struct schemas from existing ones — picking, omitting, renaming, or transforming individual fields — without rewriting the schema from scratch. The `mapFields` method on `Schema.Struct` accepts a function that transforms the struct's fields and returns a new `Schema.Struct` based on the result.
1192
1193#### Pick
1194
1195Use `Struct.pick` to keep only a selected set of fields.
1196
1197**Example** (Picking specific fields from a struct)
1198
1199```ts
1200import { Schema, Struct } from "effect"
1201
1202/*
1203const schema: Schema.Struct<{
1204 readonly a: Schema.String;
1205}>
1206*/
1207const schema = Schema.Struct({
1208 a: Schema.String,
1209 b: Schema.Number
1210}).mapFields(Struct.pick(["a"]))
1211```
1212
1213#### Omit
1214
1215Use `Struct.omit` to remove specified fields from a struct.
1216
1217**Example** (Omitting fields from a struct)
1218
1219```ts
1220import { Schema, Struct } from "effect"
1221
1222/*
1223const schema: Schema.Struct<{
1224 readonly a: Schema.String;
1225}>
1226*/
1227const schema = Schema.Struct({
1228 a: Schema.String,
1229 b: Schema.Number
1230}).mapFields(Struct.omit(["b"]))
1231```
1232
1233#### Merge
1234
1235Use `Struct.assign` to add new fields to an existing struct.
1236
1237**Example** (Adding fields to a struct)
1238
1239```ts
1240import { Schema, Struct } from "effect"
1241
1242/*
1243const schema: Schema.Struct<{
1244 readonly a: Schema.String;
1245 readonly b: Schema.Number;
1246 readonly c: Schema.Boolean;
1247}>
1248*/
1249const schema = Schema.Struct({
1250 a: Schema.String,
1251 b: Schema.Number
1252}).mapFields(
1253 Struct.assign({
1254 c: Schema.Boolean
1255 })
1256)
1257
1258// or more succinctly
1259const schema2 = Schema.Struct({
1260 a: Schema.String,
1261 b: Schema.Number
1262}).pipe(Schema.fieldsAssign({ c: Schema.Boolean }))
1263```
1264
1265If you want to preserve the checks of the original struct, you can pass `{ unsafePreserveChecks: true }` to the `map` method.
1266
1267**Warning**: This is an unsafe operation. Since `mapFields` transformations change the schema type, the original refinement functions may no longer be valid or safe to apply to the transformed schema. Only use this option if you have verified that your refinements remain correct after the transformation.
1268
1269**Example** (Preserving checks when merging fields)
1270
1271```ts
1272import { Schema, Struct } from "effect"
1273
1274const original = Schema.Struct({
1275 a: Schema.String,
1276 b: Schema.String
1277}).check(Schema.makeFilter(({ a, b }) => a === b, { title: "a === b" }))
1278
1279const schema = original.mapFields(Struct.assign({ c: Schema.String }), {
1280 unsafePreserveChecks: true
1281})
1282
1283console.log(
1284 String(
1285 Schema.decodeUnknownExit(schema)({
1286 a: "a",
1287 b: "b",
1288 c: "c"
1289 })
1290 )
1291)
1292// Failure(Cause([Fail(SchemaError: Expected a === b)]))
1293```
1294
1295#### Mapping individual fields
1296
1297Use `Struct.evolve` to transform the value schema of individual fields.
1298
1299**Example** (Modifying the type of a single field)
1300
1301```ts
1302import { Schema, Struct } from "effect"
1303
1304/*
1305const schema: Schema.Struct<{
1306 readonly a: Schema.optionalKey<Schema.String>;
1307 readonly b: Schema.Number;
1308}>
1309*/
1310const schema = Schema.Struct({
1311 a: Schema.String,
1312 b: Schema.Number
1313}).mapFields(
1314 Struct.evolve({
1315 a: (field) => Schema.optionalKey(field)
1316 })
1317)
1318```
1319
1320#### Mapping all fields at once
1321
1322If you want to transform the value schema of multiple fields at once, you can use `Struct.map`.
1323
1324**Example** (Making all fields optional)
1325
1326```ts
1327import { Schema, Struct } from "effect"
1328
1329/*
1330const schema: Schema.Struct<{
1331 readonly a: Schema.optionalKey<Schema.String>;
1332 readonly b: Schema.optionalKey<Schema.Number>;
1333 readonly c: Schema.optionalKey<Schema.Boolean>;
1334}>
1335*/
1336const schema = Schema.Struct({
1337 a: Schema.String,
1338 b: Schema.Number,
1339 c: Schema.Boolean
1340}).mapFields(Struct.map(Schema.optionalKey))
1341```
1342
1343#### Mapping a subset of fields at once
1344
1345If you want to map a subset of elements, you can use `Struct.mapPick` or `Struct.mapOmit`.
1346
1347**Example** (Making a subset of fields optional)
1348
1349```ts
1350import { Schema, Struct } from "effect"
1351
1352/*
1353const schema: Schema.Struct<{
1354 readonly a: Schema.optionalKey<Schema.String>;
1355 readonly b: Schema.Number;
1356 readonly c: Schema.optionalKey<Schema.Boolean>;
1357}>
1358*/
1359const schema = Schema.Struct({
1360 a: Schema.String,
1361 b: Schema.Number,
1362 c: Schema.Boolean
1363}).mapFields(Struct.mapPick(["a", "c"], Schema.optionalKey))
1364```
1365
1366Or if it's more convenient, you can use `Struct.mapOmit`.
1367
1368```ts
1369import { Schema, Struct } from "effect"
1370
1371/*
1372const schema: Schema.Struct<{
1373 readonly a: Schema.optionalKey<Schema.String>;
1374 readonly b: Schema.Number;
1375 readonly c: Schema.optionalKey<Schema.Boolean>;
1376}>
1377*/
1378const schema = Schema.Struct({
1379 a: Schema.String,
1380 b: Schema.Number,
1381 c: Schema.Boolean
1382}).mapFields(Struct.mapOmit(["b"], Schema.optionalKey))
1383```
1384
1385#### Mapping individual keys
1386
1387Use `Struct.evolveKeys` to rename field keys while keeping the corresponding value schemas.
1388
1389**Example** (Uppercasing keys in a struct)
1390
1391```ts
1392import { String } from "effect"
1393import { Schema } from "effect"
1394import { Struct } from "effect/data"
1395
1396/*
1397const schema: Schema.Struct<{
1398 readonly A: Schema.String;
1399 readonly b: Schema.Number;
1400}>
1401*/
1402const schema = Schema.Struct({
1403 a: Schema.String,
1404 b: Schema.Number
1405}).mapFields(
1406 Struct.evolveKeys({
1407 a: (key) => String.toUpperCase(key)
1408 })
1409)
1410```
1411
1412If you simply want to rename keys with static keys, you can use `Struct.renameKeys`.
1413
1414**Example** (Renaming keys in a struct)
1415
1416```ts
1417import { Schema, Struct } from "effect"
1418
1419/*
1420const schema: Schema.Struct<{
1421 readonly A: Schema.String;
1422 readonly b: Schema.Number;
1423}>
1424*/
1425const schema = Schema.Struct({
1426 a: Schema.String,
1427 b: Schema.Number
1428}).mapFields(
1429 Struct.renameKeys({
1430 a: "A"
1431 })
1432)
1433```
1434
1435#### Mapping individual entries
1436
1437Use `Struct.evolveEntries` when you want to transform both the key and the value of specific fields.
1438
1439**Example** (Transforming keys and value schemas)
1440
1441```ts
1442import { Schema, String, Struct } from "effect"
1443
1444/*
1445const schema: Schema.Struct<{
1446 readonly b: Schema.Number;
1447 readonly A: Schema.optionalKey<Schema.String>;
1448}>
1449*/
1450const schema = Schema.Struct({
1451 a: Schema.String,
1452 b: Schema.Number
1453}).mapFields(
1454 Struct.evolveEntries({
1455 a: (key, value) => [String.toUpperCase(key), Schema.optionalKey(value)]
1456 })
1457)
1458```
1459
1460#### Opaque Structs
1461
1462The previous examples can be applied to opaque structs as well.
1463
1464```ts
1465import { Schema, Struct } from "effect"
1466
1467class A extends Schema.Opaque<A>()(
1468 Schema.Struct({
1469 a: Schema.String,
1470 b: Schema.Number
1471 })
1472) {}
1473
1474/*
1475const schema: Schema.Struct<{
1476 readonly a: Schema.optionalKey<Schema.String>;
1477 readonly b: Schema.Number;
1478}>
1479*/
1480const schema = A.mapFields(
1481 Struct.evolve({
1482 a: (field) => Schema.optionalKey(field)
1483 })
1484)
1485```
1486
1487### Tagged Structs
1488
1489A tagged struct is a struct that includes a `_tag` field. This field is used to identify the specific variant of the object, which is especially useful when working with union types.
1490
1491When using the `make` method, the `_tag` field is optional and will be added automatically. However, when decoding or encoding, the `_tag` field must be present in the input.
1492
1493**Example** (Tagged struct as a shorthand for a struct with a `_tag` field)
1494
1495```ts
1496import { Schema } from "effect"
1497
1498// Defines a struct with a fixed `_tag` field
1499const tagged = Schema.TaggedStruct("A", {
1500 a: Schema.String
1501})
1502
1503// This is the same as writing:
1504const equivalent = Schema.Struct({
1505 _tag: Schema.tag("A"),
1506 a: Schema.String
1507})
1508```
1509
1510**Example** (Accessing the literal value of the tag)
1511
1512```ts
1513// The `_tag` field is a schema with a known literal value
1514const literal = tagged.fields._tag.schema.literal
1515// literal: "A"
1516```
1517
1518## Tuples
1519
1520A tuple schema describes a fixed-length array where each position has its own type. Use tuples when the order and count of elements matters — for example, a `[string, number]` pair.
1521
1522### Rest Elements
1523
1524You can add rest elements to a tuple using `Schema.TupleWithRest`.
1525
1526**Example** (Adding rest elements to a tuple)
1527
1528```ts
1529import { Schema } from "effect"
1530
1531export const schema = Schema.TupleWithRest(Schema.Tuple([Schema.FiniteFromString, Schema.String]), [
1532 Schema.Boolean,
1533 Schema.String
1534])
1535
1536/*
1537type Type = readonly [number, string, ...boolean[], string]
1538*/
1539type Type = typeof schema.Type
1540
1541/*
1542type Encoded = readonly [string, string, ...boolean[], string]
1543*/
1544type Encoded = typeof schema.Encoded
1545```
1546
1547### Element Annotations
1548
1549You can annotate elements using the `annotateKey` method.
1550
1551**Example** (Annotating an element)
1552
1553```ts
1554import { Schema } from "effect"
1555
1556const schema = Schema.Tuple([
1557 Schema.String.annotateKey({
1558 description: "my element description",
1559 // a message to display when the element is missing
1560 messageMissingKey: "this element is required"
1561 })
1562])
1563
1564console.log(String(Schema.decodeUnknownExit(schema)([])))
1565/*
1566Failure(Cause([Fail(SchemaError: this element is required
1567 at [0]
1568)]))
1569*/
1570```
1571
1572### Deriving Tuples
1573
1574You can map the elements of a tuple schema using the `mapElements` static method on `Schema.Tuple`. The `mapElements` static method accepts a function from `Tuple.elements` to new elements, and returns a new `Schema.Tuple` based on the result.
1575
1576#### Pick
1577
1578Use `Tuple.pick` to keep only a selected set of elements.
1579
1580**Example** (Picking specific elements from a tuple)
1581
1582```ts
1583import { Schema, Tuple } from "effect"
1584
1585/*
1586const schema: Schema.Tuple<readonly [Schema.String, Schema.Boolean]>
1587*/
1588const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.pick([0, 2]))
1589```
1590
1591#### Omit
1592
1593Use `Tuple.omit` to remove specified elements from a tuple.
1594
1595**Example** (Omitting elements from a tuple)
1596
1597```ts
1598import { Schema, Tuple } from "effect"
1599
1600/*
1601const schema: Schema.Tuple<readonly [Schema.String, Schema.Boolean]>
1602*/
1603const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.omit([1]))
1604```
1605
1606#### Adding Elements
1607
1608You can add elements to a tuple schema using the `appendElement` and `appendElements` APIs of the `Tuple` module.
1609
1610**Example** (Adding elements to a tuple)
1611
1612```ts
1613import { Schema, Tuple } from "effect"
1614
1615/*
1616const schema: Schema.Tuple<readonly [
1617 Schema.String,
1618 Schema.Number,
1619 Schema.Boolean,
1620 Schema.String,
1621 Schema.Number
1622]>
1623*/
1624const schema = Schema.Tuple([Schema.String, Schema.Number])
1625 .mapElements(Tuple.appendElement(Schema.Boolean)) // adds a single element
1626 .mapElements(Tuple.appendElements([Schema.String, Schema.Number])) // adds multiple elements
1627```
1628
1629#### Mapping individual elements
1630
1631You can evolve the elements of a tuple schema using the `evolve` API of the `Tuple` module
1632
1633**Example**
1634
1635```ts
1636import { Schema, Tuple } from "effect"
1637
1638/*
1639const schema: Schema.Tuple<readonly [
1640 Schema.NullOr<Schema.String>,
1641 Schema.Number,
1642 Schema.NullOr<Schema.Boolean>
1643]>
1644*/
1645const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(
1646 Tuple.evolve([
1647 (v) => Schema.NullOr(v),
1648 undefined, // no change
1649 (v) => Schema.NullOr(v)
1650 ])
1651)
1652```
1653
1654#### Mapping all elements at once
1655
1656You can map all elements of a tuple schema using the `map` API of the `Tuple` module.
1657
1658**Example** (Making all elements nullable)
1659
1660```ts
1661import { Schema, Tuple } from "effect"
1662
1663/*
1664const schema: Schema.Tuple<readonly [
1665 Schema.NullOr<Schema.String>,
1666 Schema.NullOr<Schema.Number>,
1667 Schema.NullOr<Schema.Boolean>
1668]>
1669*/
1670const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.map(Schema.NullOr))
1671```
1672
1673#### Mapping a subset of elements at once
1674
1675If you want to map a subset of elements, you can use `Tuple.mapPick` or `Tuple.mapOmit`.
1676
1677**Example** (Making a subset of elements nullable)
1678
1679```ts
1680import { Schema, Tuple } from "effect"
1681
1682/*
1683const schema: Schema.Tuple<readonly [
1684 Schema.NullOr<Schema.String>,
1685 Schema.Number,
1686 Schema.NullOr<Schema.Boolean>
1687]>
1688*/
1689const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(
1690 Tuple.mapPick([0, 2], Schema.NullOr)
1691)
1692```
1693
1694Or if it's more convenient, you can use `Tuple.mapOmit`.
1695
1696```ts
1697import { Schema, Tuple } from "effect"
1698
1699/*
1700const schema: Schema.Tuple<readonly [
1701 Schema.NullOr<Schema.String>,
1702 Schema.Number,
1703 Schema.NullOr<Schema.Boolean>
1704]>
1705*/
1706const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(
1707 Tuple.mapOmit([1], Schema.NullOr)
1708)
1709```
1710
1711#### Renaming Indices
1712
1713You can rename the indices of a tuple schema using the `renameIndices` API of the `Tuple` module.
1714
1715**Example** (Partial index mapping)
1716
1717```ts
1718import { Schema, Tuple } from "effect"
1719
1720/*
1721const schema: Schema.Tuple<readonly [
1722 Schema.Number,
1723 Schema.String,
1724 Schema.Boolean
1725]>
1726*/
1727const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(
1728 Tuple.renameIndices(["1", "0"]) // flip the first and second elements
1729)
1730```
1731
1732**Example** (Full index mapping)
1733
1734```ts
1735import { Schema, Tuple } from "effect"
1736
1737/*
1738const schema: Schema.Tuple<readonly [
1739 Schema.Boolean,
1740 Schema.Number,
1741 Schema.String
1742]>
1743*/
1744const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(
1745 Tuple.renameIndices([
1746 "2", // last element becomes first
1747 "1", // second element keeps its index
1748 "0" // first element becomes third
1749 ])
1750)
1751```
1752
1753## Arrays
1754
1755An array schema describes a variable-length list where every element shares the same type.
1756
1757### Mutability
1758
1759Array and tuple schemas are readonly by default. Use `Schema.mutable` to make them mutable.
1760
1761> [!NOTE]
1762> `Schema.mutable` does not support an encoding attached directly to the array or tuple schema. Apply it before adding
1763> such an encoding. Encodings on element schemas are supported.
1764
1765### Unique Arrays
1766
1767You can deduplicate arrays using `Schema.UniqueArray`.
1768
1769Internally, `Schema.UniqueArray` uses `Schema.Array` and adds a check based on `Schema.isUnique` using `ToEquivalence.make(item)` for the equivalence.
1770
1771```ts
1772import { Schema } from "effect"
1773
1774const schema = Schema.UniqueArray(Schema.String)
1775
1776console.log(String(Schema.decodeUnknownExit(schema)(["a", "b", "a"])))
1777// Failure(Cause([Fail(SchemaError: Expected an array with unique items)]))
1778```
1779
1780## Records
1781
1782A record schema describes an object whose keys are dynamic (not known ahead of time). The key schema selects which own properties belong to the record, and the value schema validates the selected property values.
1783
1784Properties that are not selected by the key schema are ignored by that record. For example, `Schema.Record(Schema.String.check(Schema.isPattern(/^a/)), Schema.Number)` decodes only string keys that start with `"a"`.
1785
1786### Key Transformations
1787
1788`Schema.Record` supports transforming keys during decoding and encoding. This can be useful when working with different naming conventions.
1789
1790When a key schema has a transformation, dynamic property selection is based on the encoded property names. The selected keys are then decoded using the key schema.
1791
1792**Example** (Transforming snake_case keys to camelCase)
1793
1794```ts
1795import { Schema, SchemaTransformation } from "effect"
1796
1797const SnakeToCamel = Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel()))
1798
1799const schema = Schema.Record(SnakeToCamel, Schema.Number)
1800
1801console.log(Schema.decodeUnknownSync(schema)({ a_b: 1, c_d: 2 }))
1802// { aB: 1, cD: 2 }
1803```
1804
1805When parsing sequentially, transformed keys are applied in selection order, so
1806the later selected property wins if a transformation produces a duplicate key.
1807With concurrency greater than `1`, completion order determines which value is
1808retained.
1809
1810**Example** (Keeping the later selected value)
1811
1812```ts
1813import { Schema, SchemaTransformation } from "effect"
1814
1815const SnakeToCamel = Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel()))
1816
1817const schema = Schema.Record(SnakeToCamel, Schema.Number)
1818
1819console.log(Schema.decodeUnknownSync(schema)({ a_b: 1, aB: 2 }))
1820// { aB: 2 }
1821```
1822
1823### Number Keys
1824
1825Records with number keys are supported.
1826
1827**Example** (Record with number keys)
1828
1829```ts
1830import { Schema } from "effect"
1831
1832const schema = Schema.Record(Schema.Int, Schema.String)
1833
1834console.log(String(Schema.decodeUnknownExit(schema)({ 1: "a", 2: "b" })))
1835// Success({"1":"a","2":"b"})
1836
1837console.log(String(Schema.decodeUnknownExit(schema)({ 1.1: "ignored" })))
1838// Success({})
1839
1840console.log(String(Schema.decodeUnknownExit(schema)({ 1: null })))
1841// Failure(Cause([Fail(SchemaError(Expected string
1842// at ["1"]))]))
1843```
1844
1845### Mutability
1846
1847By default, records are tagged as `readonly`. You can mark a record as mutable using `Schema.mutableKey` as you do with structs.
1848
1849**Example** (Defining a mutable record)
1850
1851```ts
1852import { Schema } from "effect"
1853
1854export const schema = Schema.Record(Schema.String, Schema.mutableKey(Schema.Number))
1855
1856/*
1857type Type = {
1858 [x: string]: number;
1859}
1860*/
1861type Type = typeof schema.Type
1862
1863/*
1864type Encoded = {
1865 [x: string]: number;
1866}
1867*/
1868type Encoded = typeof schema.Encoded
1869```
1870
1871### Literal Structs
1872
1873When you pass a union of string literals as the key schema to `Schema.Record`, you get a struct-like schema where each literal becomes a required key. This mirrors how TypeScript's built-in `Record` type behaves.
1874
1875**Example** (Creating a literal struct with fixed string keys)
1876
1877```ts
1878import { Schema } from "effect"
1879
1880const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.Number)
1881
1882/*
1883type Type = {
1884 readonly a: number;
1885 readonly b: number;
1886}
1887*/
1888type Type = typeof schema.Type
1889```
1890
1891#### Mutable Keys
1892
1893By default, keys are readonly. To make them mutable, use `Schema.mutableKey` just as you would with a standard struct.
1894
1895**Example** (Literal struct with mutable keys)
1896
1897```ts
1898import { Schema } from "effect"
1899
1900const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.mutableKey(Schema.Number))
1901
1902/*
1903type Type = {
1904 a: number;
1905 b: number;
1906}
1907*/
1908type Type = typeof schema.Type
1909```
1910
1911#### Optional Keys
1912
1913You can make the keys optional by wrapping the value schema with `Schema.optional`.
1914
1915**Example** (Literal struct with optional keys)
1916
1917```ts
1918import { Schema } from "effect"
1919
1920const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.optional(Schema.Number))
1921
1922/*
1923type Type = {
1924 readonly a?: number;
1925 readonly b?: number;
1926}
1927*/
1928type Type = typeof schema.Type
1929```
1930
1931## Unions
1932
1933A union schema accepts a value if it matches any one of its members. Unions are useful when a field can hold more than one type — for example, a value that is either a string or a number.
1934
1935By default, unions are _inclusive_: a value is accepted if it matches **any** of the union's members.
1936
1937The members are checked in order, and the first one that matches is used.
1938
1939### Excluding Incompatible Members
1940
1941If a union member is not compatible with the input, it is automatically excluded during validation.
1942
1943**Example** (Excluding incompatible members from the union)
1944
1945```ts
1946import { Schema } from "effect"
1947
1948const schema = Schema.Union([Schema.NonEmptyString, Schema.Number])
1949
1950console.log(String(Schema.decodeUnknownExit(schema)("")))
1951// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1)]))
1952```
1953
1954If none of the union members match the input, the union fails with a message at the top level.
1955
1956**Example** (All members excluded)
1957
1958```ts
1959import { Schema } from "effect"
1960
1961const schema = Schema.Union([Schema.NonEmptyString, Schema.Number])
1962
1963console.log(String(Schema.decodeUnknownExit(schema)(null)))
1964// Failure(Cause([Fail(SchemaError: Expected string | number)]))
1965```
1966
1967This behavior is especially helpful when working with literal values. Instead of producing a separate error for each literal (as in version 3), the schema reports a single, clear message.
1968
1969**Example** (Validating against a set of literals)
1970
1971```ts
1972import { Schema } from "effect"
1973
1974const schema = Schema.Literals(["a", "b"])
1975
1976console.log(String(Schema.decodeUnknownExit(schema)(null)))
1977// Failure(Cause([Fail(SchemaError: Expected "a" | "b")]))
1978```
1979
1980### Exclusive Unions
1981
1982You can create an exclusive union, where the union matches if exactly one member matches, by passing the `{ mode: "oneOf" }` option.
1983
1984**Example** (Exclusive Union)
1985
1986```ts
1987import { Schema } from "effect"
1988
1989const schema = Schema.Union([Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.Number })], {
1990 mode: "oneOf"
1991})
1992
1993console.log(String(Schema.decodeUnknownExit(schema)({ a: "a", b: 1 })))
1994// Failure(Cause([Fail(SchemaError: Expected exactly one member to match)]))
1995```
1996
1997### Deriving Unions
1998
1999You can map the members of a union schema using the `mapMembers` static method on `Schema.Union`. The `mapMembers` static method accepts a function from `Union.members` to new members, and returns a new `Schema.Union` based on the result.
2000
2001#### Adding Members
2002
2003You can add members to a union schema using the `appendElement` and `appendElements` APIs of the `Tuple` module.
2004
2005**Example** (Adding members to a union)
2006
2007```ts
2008import { Schema, Tuple } from "effect"
2009
2010/*
2011const schema: Schema.Union<readonly [
2012 Schema.String,
2013 Schema.Number,
2014 Schema.Boolean,
2015 Schema.String,
2016 Schema.Number
2017]>
2018*/
2019const schema = Schema.Union([Schema.String, Schema.Number])
2020 .mapMembers(Tuple.appendElement(Schema.Boolean)) // adds a single member
2021 .mapMembers(Tuple.appendElements([Schema.String, Schema.Number])) // adds multiple members
2022```
2023
2024#### Mapping individual members
2025
2026You can evolve the members of a union schema using the `evolve` API of the `Tuple` module
2027
2028**Example**
2029
2030```ts
2031import { Schema, Tuple } from "effect"
2032
2033/*
2034const schema: Schema.Union<readonly [
2035 Schema.Array$<Schema.String>,
2036 Schema.Number,
2037 Schema.Array$<Schema.Boolean>
2038]>
2039*/
2040const schema = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]).mapMembers(
2041 Tuple.evolve([
2042 (v) => Schema.Array(v),
2043 undefined, // no change
2044 (v) => Schema.Array(v)
2045 ])
2046)
2047```
2048
2049#### Mapping all members at once
2050
2051You can map all members of a union schema using the `map` API of the `Tuple` module.
2052
2053**Example**
2054
2055```ts
2056import { Schema, Tuple } from "effect"
2057
2058/*
2059const schema: Schema.Union<readonly [
2060 Schema.Array$<Schema.String>,
2061 Schema.Array$<Schema.Number>,
2062 Schema.Array$<Schema.Boolean>
2063]>
2064*/
2065const schema = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]).mapMembers(Tuple.map(Schema.Array))
2066```
2067
2068### Union of Literals
2069
2070You can create a union of literals using `Schema.Literals`.
2071
2072```ts
2073import { Schema } from "effect"
2074
2075const schema = Schema.Literals(["red", "green", "blue"])
2076```
2077
2078#### Deriving new literals
2079
2080You can map the members of a `Schema.Literals` schema using the `mapMembers` method. The `mapMembers` method accepts a function from `Literals.members` to new members, and returns a new `Schema.Union` based on the result.
2081
2082```ts
2083import { Schema, Tuple } from "effect"
2084
2085const schema = Schema.Literals(["red", "green", "blue"]).mapMembers(
2086 Tuple.evolve([
2087 (a) => Schema.Struct({ _tag: a, a: Schema.String }),
2088 (b) => Schema.Struct({ _tag: b, b: Schema.Number }),
2089 (c) => Schema.Struct({ _tag: c, c: Schema.Boolean })
2090 ])
2091)
2092
2093/*
2094type Type = {
2095 readonly _tag: "red";
2096 readonly a: string;
2097} | {
2098 readonly _tag: "green";
2099 readonly b: number;
2100} | {
2101 readonly _tag: "blue";
2102 readonly c: boolean;
2103}
2104*/
2105type Type = (typeof schema)["Type"]
2106```
2107
2108### Tagged Unions
2109
2110You can define a tagged union using the `Schema.TaggedUnion` helper. This is useful when combining multiple tagged structs into a union.
2111
2112**Example** (Defining a tagged union with `Schema.TaggedUnion`)
2113
2114```ts
2115import { Schema } from "effect"
2116
2117// Create a union of two tagged structs
2118const schema = Schema.TaggedUnion({
2119 A: { a: Schema.String },
2120 B: { b: Schema.Finite }
2121})
2122```
2123
2124This is equivalent to writing:
2125
2126```ts
2127const schema = Schema.Union([
2128 Schema.TaggedStruct("A", { a: Schema.String }),
2129 Schema.TaggedStruct("B", { b: Schema.Finite })
2130])
2131```
2132
2133The result is a tagged union schema with built-in helpers based on the tag values. See the next section for more details.
2134
2135### Augmenting Tagged Unions
2136
2137The `toTaggedUnion` function enhances a tagged union schema by adding helper methods for working with its members.
2138
2139You need to specify the name of the tag field used to differentiate between variants.
2140
2141**Example** (Adding tag-based helpers to a union)
2142
2143```ts
2144import { Schema } from "effect"
2145
2146const original = Schema.Union([
2147 Schema.Struct({ type: Schema.tag("A"), a: Schema.String }),
2148 Schema.Struct({ type: Schema.tag("B"), b: Schema.Finite }),
2149 Schema.Struct({ type: Schema.tag("C"), c: Schema.Boolean })
2150])
2151
2152// Enrich the union with tag-based utilities
2153const tagged = original.pipe(Schema.toTaggedUnion("type"))
2154```
2155
2156This helper has some advantages over a dedicated constructor:
2157
2158- It does not require changes to the original schema, just call a helper.
2159- You can apply it to schemas from external sources.
2160- You can choose among multiple possible tag fields if present.
2161- It supports unions that include nested unions.
2162
2163Each member must have a unique discriminant property key. `toTaggedUnion` throws when it encounters a duplicate. Numeric and string values that resolve to the same property key, such as `1` and `"1"`, are considered duplicates.
2164
2165**Note**. If the tag is the standard `_tag` field, you can use `Schema.TaggedUnion` instead.
2166
2167#### Accessing Members by Tag
2168
2169The `cases` property gives direct access to each member schema of the union.
2170
2171**Example** (Getting a member schema from a tagged union)
2172
2173```ts
2174const A = tagged.cases.A
2175const B = tagged.cases.B
2176const C = tagged.cases.C
2177```
2178
2179#### Accessing Discriminant Values
2180
2181The `discriminants` property contains the decoded discriminant values in the same order as the flattened union members. Its type preserves the exact tuple of values.
2182
2183**Example** (Deriving a literal schema from discriminants)
2184
2185```ts
2186const Tags = Schema.Literals(tagged.discriminants)
2187
2188// Schema.Literals<readonly ["A", "B", "C"]>
2189```
2190
2191#### Checking Membership in a Subset of Tags
2192
2193The `isAnyOf` method lets you check if a value belongs to a selected subset of tags.
2194
2195**Example** (Checking membership in a subset of union tags)
2196
2197```ts
2198console.log(tagged.isAnyOf(["A", "B"])({ type: "A", a: "a" })) // true
2199console.log(tagged.isAnyOf(["A", "B"])({ type: "B", b: 1 })) // true
2200
2201console.log(tagged.isAnyOf(["A", "B"])({ type: "C", c: true })) // false
2202```
2203
2204#### Type Guards
2205
2206The `guards` property provides a type guard for each tag.
2207
2208**Example** (Using type guards for tagged members)
2209
2210```ts
2211console.log(tagged.guards.A({ type: "A", a: "a" })) // true
2212console.log(tagged.guards.B({ type: "B", b: 1 })) // true
2213
2214console.log(tagged.guards.A({ type: "B", b: 1 })) // false
2215```
2216
2217#### Matching on a Tag
2218
2219You can define a matcher function using the `match` method. This is a concise way to handle each variant of the union.
2220
2221**Example** (Handling union members with `match`)
2222
2223```ts
2224const matcher = tagged.match({
2225 A: (a) => `This is an A: ${a.a}`,
2226 B: (b) => `This is a B: ${b.b}`,
2227 C: (c) => `This is a C: ${c.c}`
2228})
2229
2230console.log(matcher({ type: "A", a: "a" })) // This is an A: a
2231console.log(matcher({ type: "B", b: 1 })) // This is a B: 1
2232console.log(matcher({ type: "C", c: true })) // This is a C: true
2233```
2234
2235## Recursive Schemas
2236
2237Use `Schema.suspend` when a schema needs to refer to itself (or to another schema that eventually refers back). `suspend` wraps a thunk, so the recursive reference is resolved lazily during decode / encode instead of eagerly during declaration.
2238
2239**Example** (Recursive Struct with Same Encoded and Type)
2240
2241```ts
2242import { Schema } from "effect"
2243
2244interface Category {
2245 readonly name: string
2246 readonly children: ReadonlyArray<Category>
2247}
2248
2249const Category: Schema.Codec<Category> = Schema.Struct({
2250 name: Schema.String,
2251 children: Schema.Array(Schema.suspend((): Schema.Codec<Category> => Category))
2252})
2253```
2254
2255The explicit `Schema.Codec<Category>` annotation is important in recursive declarations because `Category` is referenced inside its own initializer. Without the annotation, TypeScript often cannot stabilize the self-referential type and falls back to an implicit `any` style error.
2256
2257**Example** (Recursive Struct with Different Encoded and Type)
2258
2259```ts
2260import { Schema } from "effect"
2261
2262interface Category {
2263 readonly name: number
2264 readonly children: ReadonlyArray<Category>
2265}
2266
2267interface CategoryEncoded {
2268 readonly name: string
2269 readonly children: ReadonlyArray<CategoryEncoded>
2270}
2271
2272const Category: Schema.Codec<Category, CategoryEncoded> = Schema.Struct({
2273 name: Schema.FiniteFromString,
2274 children: Schema.Array(Schema.suspend((): Schema.Codec<Category, CategoryEncoded> => Category))
2275})
2276```
2277
2278Here the encoded shape differs from the runtime shape (`name` is `string` when encoded, `number` after decoding), so both type parameters must be explicit: `Schema.Codec<Category, CategoryEncoded>`.
2279
2280Using only `Schema.Codec<Category>` would force encoded and decoded types to be the same, which does not describe this schema.
2281
2282**Example** (Recursive Union)
2283
2284```ts
2285import { Schema } from "effect"
2286
2287type U = A | B
2288
2289interface A {
2290 readonly a: string
2291 readonly next: U
2292}
2293interface B {
2294 readonly b: number
2295 readonly next: U
2296}
2297
2298const URef = Schema.suspend((): Schema.Codec<U> => U)
2299
2300const A: Schema.Codec<A> = Schema.Struct({
2301 a: Schema.String,
2302 next: URef
2303})
2304
2305const B: Schema.Codec<B> = Schema.Struct({
2306 b: Schema.Number,
2307 next: URef
2308})
2309
2310const U: Schema.Codec<U> = Schema.Union([A, B])
2311```
2312
2313`URef` factors the recursive edge (`U -> U`) into one shared `Schema.suspend` value. Reusing it across members avoids duplicating the lazy reference and makes the intent clear: every variant points back to the same union schema.
2314
2315# Declaring Custom Types
2316
2317When none of the built-in schema combinators fit your data type, use `Schema.declare` or `Schema.declareConstructor`.
2318
2319## `Schema.declare` (non-parametric types)
2320
2321`Schema.declare` creates a schema from a **type guard** — a function that checks whether an unknown value is of a given type. This is useful when you have a type that doesn't fit the built-in combinators (like `Struct`, `Array`, etc.) and you need to teach Schema how to recognize it.
2322
2323```ts
2324Schema.declare<T>(
2325 is: (u: unknown) => u is T,
2326 annotations?: { expected?: string; toCodecJson?: ...; ... }
2327)
2328```
2329
2330The first argument is your type guard. Schema will call it on any input value: if it returns `true`, decoding succeeds; if `false`, decoding fails.
2331
2332**Example** (Creating a schema for `URL`)
2333
2334```ts
2335import { Schema } from "effect"
2336
2337// The type guard tells Schema how to recognize a URL instance
2338const URLSchema = Schema.declare(
2339 (u): u is URL => u instanceof URL
2340)
2341
2342console.log(String(Schema.decodeUnknownExit(URLSchema)(new URL("https://example.com"))))
2343// Success(https://example.com/)
2344
2345console.log(String(Schema.decodeUnknownExit(URLSchema)(null)))
2346// Failure(Cause([Fail(SchemaError(Expected <Declaration>))]))
2347```
2348
2349> **Tip**: For simple `instanceof` checks, prefer `Schema.instanceOf(URL)`, it wraps `Schema.declare` with an `instanceof` guard automatically.
2350
2351### Customizing the error message with `expected`
2352
2353The default error message `Expected <Declaration>` is not very descriptive. Use the `expected` annotation (second argument) to provide a human-readable name for your type.
2354
2355**Example** (Adding an `expected` annotation)
2356
2357```ts
2358import { Schema } from "effect"
2359
2360const URLSchema = Schema.declare(
2361 (u): u is URL => u instanceof URL,
2362 { expected: "URL" }
2363)
2364
2365console.log(String(Schema.decodeUnknownExit(URLSchema)(null)))
2366// Failure(Cause([Fail(SchemaError(Expected URL))]))
2367// ^^^
2368// Now the error message shows "URL" instead of "<Declaration>"
2369```
2370
2371### Adding JSON support with `toCodecJson`
2372
2373`Schema.toCodecJson` derives a codec that can convert your type **to and from JSON**. By default, declared schemas use `Schema.Json` as their encoded representation. This keeps codec derivation defined, but it does not invent a serialization for your custom type. For example, a `URL` instance is not itself a JSON value, so encoding fails:
2374
2375```ts
2376import { Schema } from "effect"
2377
2378const URLSchema = Schema.declare(
2379 (u): u is URL => u instanceof URL,
2380 { expected: "URL" }
2381)
2382
2383// Derive a JSON codec from the schema
2384const codec = Schema.toCodecJson(URLSchema)
2385
2386// Encoding fails because Schema doesn't know how to serialize a URL to JSON yet
2387console.log(String(Schema.encodeUnknownExit(codec)(new URL("https://example.com"))))
2388// Failure(Cause([Fail(SchemaError(Expected JSON value))]))
2389```
2390
2391To fix this, provide a `toCodecJson` annotation. This annotation is a function that returns an `AST.Link`, a bridge that describes how to convert between your custom type and a JSON-friendly representation.
2392
2393You build a `Link` using `Schema.link<T>()`, which takes two arguments:
2394
23951. **A JSON-side schema** — the shape of the JSON value (e.g. `Schema.String` for a URL string)
23962. **A transformation** — how to convert back and forth between your type and the JSON value
2397
2398The annotation only defines the link; the consuming parser chooses its execution and failure handling. Transformations may be asynchronous, may fail, and may use optional services, but cannot require services not declared by the input schema.
2399
2400**Example** (Making `URL` JSON-serializable)
2401
2402```ts
2403import { Effect, Schema, SchemaIssue, SchemaTransformation } from "effect"
2404
2405const URLSchema = Schema.declare(
2406 (u): u is URL => u instanceof URL,
2407 {
2408 expected: "URL",
2409 // Teach Schema how to convert URL <-> JSON
2410 toCodecJson: () =>
2411 Schema.link<globalThis.URL>()(
2412 // The JSON representation is a plain string
2413 Schema.String,
2414 // How to convert between URL and string
2415 SchemaTransformation.transformEffect<URL, string>({
2416 // JSON string -> URL (may fail if the string is not a valid URL)
2417 decode: (s, options) =>
2418 Effect.try({
2419 try: () => new URL(s),
2420 catch: () => new SchemaIssue.InvalidValue({ message: "Invalid URL string" }, s, options)
2421 }),
2422 // URL -> JSON string (always succeeds)
2423 encode: (url) => Effect.succeed(url.href)
2424 })
2425 )
2426 }
2427)
2428
2429const codec = Schema.toCodecJson(URLSchema)
2430
2431// Now encoding produces the URL's href string
2432console.log(String(Schema.encodeUnknownExit(codec)(new URL("https://example.com"))))
2433// Success("https://example.com/")
2434
2435// And decoding parses a string back into a URL
2436console.log(String(Schema.decodeUnknownExit(codec)("https://example.com")))
2437// Success(https://example.com/)
2438```
2439
2440## `Schema.declareConstructor` (parametric types)
2441
2442While `Schema.declare` works for fixed types like `URL` or `File`, some types are **generic** — they contain other types as parameters. Think of `Array<A>`, `Option<A>`, or a custom `Box<A>`. The schema for `Box<number>` is different from `Box<string>` because the inner value has a different type.
2443
2444`Schema.declareConstructor` handles this by letting you define a **schema factory**: a function that takes schemas for the type parameters and returns a schema for the full type.
2445
2446> **Important:** `declareConstructor` is for types where the **container shape is the same** on both sides: only the inner type parameter changes (e.g. `Box<Encoded>` to `Box<Type>`). If you need to convert a structurally different type into your declared type (e.g. `T` to `Box<T>`), first declare `Box` with `declareConstructor`, then define a separate transformation schema to express the conversion.
2447
2448### How the two-step call works
2449
2450`declareConstructor` uses a curried (two-step) call pattern:
2451
2452```ts
2453Schema.declareConstructor<Type, Encoded>()(
2454 typeParameters, // array of schemas, one per type parameter
2455 run, // factory that produces the parsing function
2456 annotations // optional metadata (same as Schema.declare)
2457)
2458```
2459
24601. **Outer call** `declareConstructor<Type, Encoded>()` — fixes the TypeScript types. `Type` is the decoded type, `Encoded` is the encoded type.
24612. **Inner call** `(typeParameters, run, annotations)` — provides the runtime behavior:
2462 - `typeParameters` — an array of schemas, one for each type variable (e.g. `[itemSchema]` for `Box<A>`)
2463 - `run` — a function that receives **resolved codecs** for those type parameters and returns a **parsing function** `(input, ast, options) => Effect<T, Issue>`
2464 - `annotations` — optional metadata like `expected`, `toCodecJson`, etc.
2465
2466The parsing function you return from `run` is responsible for:
2467
24681. Checking that the input has the right shape (e.g. is an object with a `value` property)
24692. Recursively decoding inner values using the provided codecs
24703. Returning an `Effect` that succeeds with the decoded value or fails with an issue
2471
2472**Example** (A generic `Box<A>` container)
2473
2474```ts
2475import { Effect, Schema, SchemaIssue, SchemaParser } from "effect"
2476
2477// 1. Define the type
2478interface Box<A> {
2479 readonly value: A
2480}
2481
2482// 2. A type guard that checks the shape (ignoring the inner type)
2483const isBox = (u: unknown): u is Box<unknown> => typeof u === "object" && u !== null && "value" in u
2484
2485// 3. Create a schema factory: given a schema for A, return a schema for Box<A>
2486const Box = <A extends Schema.Top>(item: A) =>
2487 Schema.declareConstructor<Box<A["Type"]>, Box<A["Encoded"]>>()(
2488 // Pass the inner schema as a type parameter
2489 [item],
2490 // `run` receives the resolved codec for `item`
2491 ([itemCodec]) =>
2492 // Return the parsing function
2493 (u, ast, options) => {
2494 // First, check the outer shape
2495 if (!isBox(u)) {
2496 return Effect.fail(new SchemaIssue.InvalidType(ast, u, options))
2497 }
2498 // Then, decode the inner value using the item codec
2499 return Effect.mapBothEager(
2500 SchemaParser.decodeUnknownEffect(itemCodec)(u.value, options),
2501 {
2502 onSuccess: (value) => ({ value }),
2503 // Wrap inner errors with a Pointer so the error path shows ["value"]
2504 onFailure: (issue) => new SchemaIssue.Pointer(["value"], issue)
2505 }
2506 )
2507 }
2508 )
2509
2510// Use it: Box<number> that decodes strings to finite numbers
2511const schema = Box(Schema.FiniteFromString)
2512
2513console.log(String(Schema.decodeUnknownExit(schema)({ value: "1" })))
2514// Success({ value: 1 })
2515
2516console.log(String(Schema.decodeUnknownExit(schema)({ value: "a" })))
2517// Failure(Cause([Fail(SchemaError(Expected a finite number
2518// at ["value"]))]))
2519```
2520
2521> `declareConstructor` accepts the same `annotations` as `declare` — including `expected` (for custom error messages) and `toCodecJson` (for JSON serialization). See the [`Schema.declare` section above](#schemadeclare-non-parametric-types) for details on how to use them.
2522
2523# Validation
2524
2525After defining a schema's shape, you can add validation rules called _filters_. Filters check runtime values against constraints like minimum length, numeric range, or custom predicates. Validation happens at runtime — Schema checks the actual value against the rules you define and reports any violations.
2526
2527You can apply filters with the `.check` method or the `Schema.check` function.
2528
2529Define custom filters with `Schema.makeFilter`.
2530
2531**Example** (Custom filter that checks minimum length)
2532
2533```ts
2534import { Schema } from "effect"
2535
2536// Filter: the string must have at least 3 characters
2537const schema = Schema.String.check(Schema.makeFilter((s) => s.length >= 3))
2538
2539console.log(String(Schema.decodeUnknownExit(schema)("")))
2540// Failure(Cause([Fail(SchemaError: Expected <filter>)]))
2541```
2542
2543You can attach annotations and provide a custom error message when defining a filter.
2544
2545**Example** (Filter with annotations and a custom message)
2546
2547```ts
2548import { Schema } from "effect"
2549
2550// Filter with a title, description, and custom error message
2551const schema = Schema.String.check(
2552 Schema.makeFilter((s) => s.length >= 3 || `length must be >= 3, got ${s.length}`, {
2553 title: "length >= 3",
2554 description: "a string with at least 3 characters"
2555 })
2556)
2557
2558console.log(String(Schema.decodeUnknownExit(schema)("")))
2559// Failure(Cause([Fail(SchemaError: length must be >= 3, got 0)]))
2560```
2561
2562### Filter error messages and schema identifiers
2563
2564The default formatter chooses the error label from the level that failed:
2565
2566- If the input does not match the base schema type, the formatter reports a
2567 type-level failure. In that case, a schema `identifier` is used as the
2568 expected label.
2569- If the base type matches but a filter fails, the formatter reports a filter
2570 failure. In that case, the filter's `message` annotation is used first, then
2571 its `expected` annotation, and finally `<filter>` if neither is provided.
2572
2573An `identifier` does not name a failed filter. Use `expected` to name the
2574filter in the default formatter, or `message` to replace the filter failure
2575message completely.
2576
2577**Example** (Schema identifier versus filter expected message)
2578
2579```ts
2580import { Schema } from "effect"
2581
2582const Username = Schema.NonEmptyString.annotate({ identifier: "Username" })
2583
2584console.log(String(Schema.decodeUnknownExit(Username)(null)))
2585// Failure(Cause([Fail(SchemaError: Expected Username)]))
2586
2587console.log(String(Schema.decodeUnknownExit(Username)("")))
2588// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1)]))
2589```
2590
2591### Filter return shapes
2592
2593A filter predicate can return any of the shapes described by `Schema.FilterOutput`:
2594
2595- `undefined` or `true` — success.
2596- `false` — generic failure (no custom message).
2597- `string` — failure with the string used as the error message.
2598- `SchemaIssue.Issue` — a fully-formed issue, returned as-is (escape hatch for `Composite`, `AnyOf`, etc.).
2599- `{ path, issue }` — failure attached to a nested path. `issue` can be a `string` (wrapped in an `InvalidValue`) or a full `SchemaIssue.Issue`.
2600- `ReadonlyArray<FilterIssue>` — several failures reported together. Empty arrays are success; a single element is unwrapped; multiple entries are grouped into an `Issue.Composite`.
2601
2602**Example** (Failure at a nested path)
2603
2604```ts
2605import { Schema } from "effect"
2606
2607const schema = Schema.Struct({ password: Schema.String, confirmPassword: Schema.String }).check(
2608 Schema.makeFilter((o) =>
2609 o.password === o.confirmPassword
2610 ? undefined
2611 : { path: ["password"], issue: "password and confirmPassword must match" }
2612 )
2613)
2614
2615console.log(String(Schema.decodeUnknownExit(schema)({ password: "123456", confirmPassword: "1234567" })))
2616// Failure(Cause([Fail(SchemaError: password and confirmPassword must match
2617// at ["password"])]))
2618```
2619
2620**Example** (Reporting multiple failures at once)
2621
2622```ts
2623import { Schema } from "effect"
2624
2625const schema = Schema.Struct({ a: Schema.Finite, b: Schema.Finite, c: Schema.Finite }).check(
2626 Schema.makeFilter((o) => {
2627 const issues: Array<Schema.FilterIssue> = []
2628 if (o.a > 0) {
2629 if (o.b <= 0) issues.push({ path: ["b"], issue: "b must be greater than 0" })
2630 if (o.c <= 0) issues.push({ path: ["c"], issue: "c must be greater than 0" })
2631 }
2632 return issues
2633 })
2634)
2635
2636console.log(String(Schema.decodeUnknownExit(schema)({ a: 1, b: 0, c: 0 })))
2637// Failure(Cause([Fail(SchemaError: b must be greater than 0
2638// at ["b"]
2639// c must be greater than 0
2640// at ["c"])]))
2641```
2642
2643## Preserving Schema Type After Filtering
2644
2645Adding a filter does not change the schema's type. You can still use all schema-specific methods (like `.fields` on a struct or `.make`) after calling `.check(...)`.
2646
2647**Example** (Chaining filters and annotations without losing type information)
2648
2649```ts
2650import { Schema } from "effect"
2651
2652// ┌─── Schema.String
2653// ▼
2654Schema.String
2655
2656// ┌─── Schema.String
2657// ▼
2658const NonEmptyString = Schema.String.check(Schema.isNonEmpty())
2659
2660// ┌─── Schema.String
2661// ▼
2662const schema = NonEmptyString.annotate({})
2663```
2664
2665Even after adding a filter and an annotation, the schema is still a `Schema.String`.
2666
2667**Example** (Accessing struct fields after filtering)
2668
2669```ts
2670import { Schema } from "effect"
2671
2672// Define a struct and apply a (dummy) filter
2673const schema = Schema.Struct({
2674 name: Schema.String,
2675 age: Schema.Number
2676}).check(Schema.makeFilter(() => true))
2677
2678// The `.fields` property is still available
2679const fields = schema.fields
2680```
2681
2682## Filters as First-Class
2683
2684Filters are standalone values that you can define once and reuse across different schemas. The same filter (for example, `Schema.isMinLength`) works on strings, arrays, or any type with a compatible shape.
2685
2686You can pass multiple filters to a single `.check(...)` call.
2687
2688**Example** (Combining filters on a string)
2689
2690```ts
2691import { Schema } from "effect"
2692
2693const schema = Schema.String.check(
2694 Schema.isMinLength(3), // value must be at least 3 chars long
2695 Schema.isTrimmed() // no leading/trailing whitespace
2696)
2697
2698console.log(String(Schema.decodeUnknownExit(schema)(" a")))
2699// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3)]))
2700```
2701
2702**Example** (Using `isMinLength` with an object that has `length`)
2703
2704```ts
2705import { Schema } from "effect"
2706
2707// Object must have a numeric `length` field that is >= 3
2708const schema = Schema.Struct({ length: Schema.Number }).check(Schema.isMinLength(3))
2709
2710console.log(String(Schema.decodeUnknownExit(schema)({ length: 2 })))
2711// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3)]))
2712```
2713
2714**Example** (Validating array length)
2715
2716```ts
2717import { Schema } from "effect"
2718
2719// Array must contain at least 3 strings
2720const schema = Schema.Array(Schema.String).check(Schema.isMinLength(3))
2721
2722console.log(String(Schema.decodeUnknownExit(schema)(["a", "b"])))
2723// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3)]))
2724```
2725
2726## Multiple Issues Reporting
2727
2728By default, when `{ errors: "all" }` is passed, all filters are evaluated, even if one fails. This allows multiple issues to be reported at once.
2729
2730**Example** (Collecting multiple validation issues)
2731
2732```ts
2733import { Schema } from "effect"
2734
2735const schema = Schema.String.check(Schema.isMinLength(3), Schema.isTrimmed())
2736
2737console.log(
2738 String(
2739 Schema.decodeUnknownExit(schema)(" a", {
2740 errors: "all"
2741 })
2742 )
2743)
2744/*
2745Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3
2746Expected a string with no leading or trailing whitespace)]))
2747*/
2748```
2749
2750## Aborting Validation
2751
2752If you want to stop validation as soon as a filter fails, you can call the `abort` method on the filter.
2753
2754**Example** (Short-circuit on first failure)
2755
2756```ts
2757import { Schema } from "effect"
2758
2759const schema = Schema.String.check(
2760 Schema.isMinLength(3).abort(), // Stop on failure here
2761 Schema.isTrimmed() // This will not run if minLength fails
2762)
2763
2764console.log(
2765 String(
2766 Schema.decodeUnknownExit(schema)(" a", {
2767 errors: "all"
2768 })
2769 )
2770)
2771// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3)]))
2772```
2773
2774## Filter Groups
2775
2776Group filters into a reusable unit with `Schema.makeFilterGroup`. This helps when the same set of checks appears in multiple places.
2777
2778**Example** (Reusable group for 32-bit integers)
2779
2780```ts
2781import { Schema } from "effect"
2782
2783// ┌─── FilterGroup<number>
2784// ▼
2785const isInt32 = Schema.makeFilterGroup(
2786 [Schema.isInt(), Schema.isBetween({ minimum: -2147483648, maximum: 2147483647 })],
2787 {
2788 title: "isInt32",
2789 description: "a 32-bit integer"
2790 }
2791)
2792```
2793
2794## Refinements
2795
2796Use `Schema.refine` to refine a schema to a more specific type.
2797
2798**Example** (Require at least two items in a string array)
2799
2800```ts
2801import { Schema } from "effect"
2802
2803// ┌─── refine<readonly [string, string, ...string[]], Schema.Array$<Schema.String>>
2804// ▼
2805const refined = Schema.Array(Schema.String).pipe(
2806 Schema.refine((arr): arr is readonly [string, string, ...Array<string>] => arr.length >= 2)
2807)
2808```
2809
2810## Branding
2811
2812Use `Schema.brand` to add a brand to a schema.
2813
2814The identifier must be a single concrete string literal. `Schema.brand` adds
2815a nominal distinction to the decoded TypeScript type. It does not add runtime
2816validation or metadata to the schema AST. Apply it once per identifier when a
2817type has multiple brands.
2818
2819Because branding is type-only, `SchemaRepresentation` does not preserve it.
2820Reapply `Schema.brand` after rebuilding a representation or generating schema
2821code when the branded TypeScript type is still required.
2822
2823**Example** (Brand a string as a UserId)
2824
2825```ts
2826import { Schema } from "effect"
2827
2828// ┌─── Schema.brand<Schema.String, "UserId">
2829// ▼
2830const branded = Schema.String.pipe(Schema.brand("UserId"))
2831```
2832
2833### Using Brand constructors
2834
2835Use `Schema.fromBrand` to reuse the checks from a `Brand.Constructor`. The
2836constructor must have exactly one concrete brand key, and the identifier must
2837match that key. Apply `Schema.fromBrand` once per constructor to compose
2838distinct brands. Use `Schema.Union` for alternatives instead.
2839
2840With a string enum brand key, pass the enum member rather than its string value.
2841
2842**Example** (Compose checked brands)
2843
2844```ts
2845import { Brand, Schema } from "effect"
2846
2847type Int = number & Brand.Brand<"Int">
2848const Int = Brand.check<Int>(Schema.isInt())
2849
2850type Positive = number & Brand.Brand<"Positive">
2851const Positive = Brand.check<Positive>(Schema.isGreaterThan(0))
2852
2853const PositiveInt = Schema.Number.pipe(
2854 Schema.fromBrand("Int", Int),
2855 Schema.fromBrand("Positive", Positive)
2856)
2857```
2858
2859## Structural Filters
2860
2861Some filters check the structure of a value rather than its contents — for example, the number of items in an array or the number of keys in an object. These are called **structural filters**.
2862
2863Examples include:
2864
2865- `isMinLength` or `isMaxLength` on arrays
2866- `isMinSize` or `isMaxSize` on objects with a `size` property
2867- `isMinProperties` or `isMaxProperties` on objects
2868- any constraint that applies to the "shape" of a value rather than to its nested values
2869
2870Structural filters run only after the base array, object, or declaration and its nested values parse successfully. If a nested value fails, its issue is reported but structural filters on the containing value are not evaluated, even with `{ errors: "all" }`.
2871
2872**Example** (A nested failure prevents the structural filter from running)
2873
2874```ts
2875import { Schema } from "effect"
2876
2877const schema = Schema.Struct({
2878 tags: Schema.Array(Schema.String.check(Schema.isNonEmpty())).check(
2879 Schema.isMinLength(3) // structural filter
2880 )
2881})
2882
2883console.log(String(Schema.decodeUnknownExit(schema)({ tags: ["a", ""] }, { errors: "all" })))
2884/*
2885Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1
2886 at ["tags"][1])]))
2887*/
2888```
2889
2890## Effectful Filters
2891
2892Filters passed to `.check(...)` must be synchronous. When you need to call an API or use a service during validation, use an effectful filter instead. Effectful filters run inside an `Effect`, which means they can be asynchronous and access services.
2893
2894Define an effectful filter with `Getter.checkEffect` as part of a transformation.
2895
2896**Example** (Asynchronous validation of a numeric value)
2897
2898```ts
2899import { Effect, Result, Schema, SchemaGetter, SchemaIssue } from "effect"
2900
2901// Simulated API call that fails when userId is 0
2902const myapi = (userId: number) =>
2903 Effect.gen(function*() {
2904 if (userId === 0) {
2905 return new Error("not found")
2906 }
2907 return { userId }
2908 }).pipe(Effect.delay(100))
2909
2910const schema = Schema.Finite.pipe(
2911 Schema.decode({
2912 decode: SchemaGetter.checkEffect((n, options) =>
2913 Effect.gen(function*() {
2914 // Call the async API and wrap the result in a Result
2915 const user = yield* Effect.result(myapi(n))
2916
2917 // If the result is an error, return a SchemaIssue
2918 return Result.isFailure(user)
2919 ? new SchemaIssue.InvalidValue({ message: "not found" }, n, options)
2920 : undefined // No issue, value is valid
2921 })
2922 ),
2923 encode: SchemaGetter.passthrough()
2924 })
2925)
2926```
2927
2928## Filter Factories
2929
2930A filter factory is a function that returns a new filter each time you call it, letting you parameterize the constraint (for example, "greater than X" for any value of X).
2931
2932**Example** (Factory for a `isGreaterThan` filter on ordered values)
2933
2934```ts
2935import { Order, Schema } from "effect"
2936
2937// Create a filter factory for values greater than a given value
2938export const makeGreaterThan = <T>(options: {
2939 readonly order: Order.Order<T>
2940 readonly annotate?: ((exclusiveMinimum: T) => Schema.Annotations.Filter) | undefined
2941 readonly format?: (value: T) => string | undefined
2942}) => {
2943 const greaterThan = Order.isGreaterThan(options.order)
2944 const format = options.format ?? globalThis.String
2945 return (exclusiveMinimum: T, annotations?: Schema.Annotations.Filter) => {
2946 return Schema.makeFilter<T>((input) => greaterThan(input, exclusiveMinimum), {
2947 title: `greaterThan(${format(exclusiveMinimum)})`,
2948 description: `a value greater than ${format(exclusiveMinimum)}`,
2949 ...options.annotate?.(exclusiveMinimum),
2950 ...annotations
2951 })
2952 }
2953}
2954```
2955
2956# Constructors
2957
2958A constructor creates a value of the schema's type, running all validations at the time of creation. If the value does not satisfy the schema, the constructor throws an error. Every schema exposes a `make` method for this purpose.
2959
2960For an alternative that does not throw on schema validation failures, use `Schema.makeOption` (or `SchemaParser.makeOption`), which returns `Option.Some` on success and `Option.None` for schema issues. Non-schema failures, such as defects, still throw.
2961
2962```ts
2963import { Schema, SchemaParser } from "effect"
2964
2965const schema = Schema.Struct({
2966 a: Schema.Number.check(Schema.isGreaterThan(0))
2967})
2968
2969console.log(schema.makeOption({ a: 1 }))
2970// { _id: 'Option', _tag: 'Some', value: { a: 1 } }
2971
2972console.log(schema.makeOption({ a: -1 }))
2973// { _id: 'Option', _tag: 'None' }
2974
2975// Equivalent standalone usage:
2976const parse = SchemaParser.makeOption(schema)
2977
2978console.log(parse({ a: 1 }))
2979// { _id: 'Option', _tag: 'Some', value: { a: 1 } }
2980```
2981
2982## Constructors in Composed Schemas
2983
2984To support constructing values from composed schemas, `make` is now available on all schemas, including unions.
2985
2986```ts
2987import { Schema } from "effect"
2988
2989const schema = Schema.Union([Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.Number })])
2990
2991schema.make({ a: "hello" })
2992schema.make({ b: 1 })
2993```
2994
2995## Branded Constructors
2996
2997Branding adds an invisible marker to a type so that values from different domains cannot be accidentally mixed — even when they have the same underlying shape (for example, both are `string`). For branded schemas, the default constructor accepts an unbranded input and returns a branded output.
2998
2999```ts
3000import { Schema } from "effect"
3001
3002const schema = Schema.String.pipe(Schema.brand<"a">())
3003
3004// make(input: string, options?: Schema.MakeOptions): string & Brand<"a">
3005schema.make
3006```
3007
3008However, when a branded schema is part of a composite (such as a struct), you must pass a branded value.
3009
3010```ts
3011import { Schema } from "effect"
3012
3013const schema = Schema.Struct({
3014 a: Schema.String.pipe(Schema.brand<"a">()),
3015 b: Schema.Number
3016})
3017
3018/*
3019make(input: {
3020 readonly a: string & Brand<"a">;
3021 readonly b: number;
3022}, options?: Schema.MakeOptions): {
3023 readonly a: string & Brand<"a">;
3024 readonly b: number;
3025}
3026*/
3027schema.make
3028```
3029
3030## Refined Constructors
3031
3032For refined schemas, the constructor accepts the unrefined type and returns the refined one.
3033
3034```ts
3035import { Option, Schema } from "effect"
3036
3037const schema = Schema.Option(Schema.String).pipe(Schema.refine(Option.isSome))
3038
3039// make(input: Option.Option<string>, options?: Schema.MakeOptions): Option.Some<string>
3040schema.make
3041```
3042
3043As with branding, when used in a composite schema, the refined value must be provided.
3044
3045```ts
3046import { Option, Schema } from "effect"
3047
3048const schema = Schema.Struct({
3049 a: Schema.Option(Schema.String).pipe(Schema.refine(Option.isSome)),
3050 b: Schema.Number
3051})
3052
3053/*
3054make(input: {
3055 readonly a: Option.Some<string>;
3056 readonly b: number;
3057}, options?: Schema.MakeOptions): {
3058 readonly a: Option.Some<string>;
3059 readonly b: number;
3060}
3061*/
3062schema.make
3063```
3064
3065## Default Values in Constructors
3066
3067You can define a default value for a field using `Schema.withConstructorDefault`. If no value is provided at runtime (either the key is missing or the value is `undefined`), the constructor uses this default.
3068
3069**Example** (Providing a default number)
3070
3071```ts
3072import { Effect, Schema } from "effect"
3073
3074const schema = Schema.Struct({
3075 a: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
3076})
3077
3078console.log(schema.make({ a: 5 }))
3079// { a: 5 }
3080
3081console.log(schema.make({}))
3082// { a: -1 }
3083```
3084
3085The Effect passed to `withConstructorDefault` will be executed each time a default value is needed.
3086
3087**Example** (Re-executing the default function)
3088
3089```ts
3090import { Effect, Schema } from "effect"
3091
3092let counter = 0
3093
3094const schema = Schema.Struct({
3095 a: Schema.Date.pipe(Schema.withConstructorDefault(Effect.sync(() => new Date(counter++))))
3096})
3097
3098console.log(schema.make({}))
3099// { a: 1970-01-01T00:00:00.000Z }
3100
3101console.log(schema.make({}))
3102// { a: 1970-01-01T00:00:00.001Z }
3103```
3104
3105### Nested Constructor Default Values
3106
3107Default values can be nested inside composed schemas. In this case, inner defaults are resolved first.
3108
3109**Example** (Nested default values)
3110
3111```ts
3112import { Effect, Schema } from "effect"
3113
3114const schema = Schema.Struct({
3115 a: Schema.Struct({
3116 b: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
3117 }).pipe(Schema.withConstructorDefault(Effect.succeed({})))
3118})
3119
3120console.log(schema.make({}))
3121// { a: { b: -1 } }
3122console.log(schema.make({ a: {} }))
3123// { a: { b: -1 } }
3124```
3125
3126## Effectful Defaults
3127
3128Default values can also come from an `Effect`, for example, reading from a configuration service or performing an asynchronous operation. The environment must be `never` (no required services).
3129
3130**Example** (Using an effect to provide a default)
3131
3132```ts
3133import { Effect, Schema, SchemaParser } from "effect"
3134
3135const schema = Schema.Struct({
3136 a: Schema.Number.pipe(
3137 Schema.withConstructorDefault(
3138 Effect.gen(function*() {
3139 yield* Effect.sleep(100)
3140 return -1
3141 })
3142 )
3143 )
3144})
3145
3146SchemaParser.makeEffect(schema)({}).pipe(Effect.runPromise).then(console.log)
3147// { a: -1 }
3148```
3149
3150**Example** (Providing a default from an optional service)
3151
3152```ts
3153import { Context, Effect, Option, Schema, SchemaParser } from "effect"
3154
3155// Define a service that may provide a default value
3156class ConstructorService extends Context.Service<ConstructorService, { defaultValue: Effect.Effect<number> }>()(
3157 "ConstructorService"
3158) {}
3159
3160const schema = Schema.Struct({
3161 a: Schema.Number.pipe(
3162 Schema.withConstructorDefault(
3163 Effect.gen(function*() {
3164 yield* Effect.sleep(100)
3165 const oservice = yield* Effect.serviceOption(ConstructorService)
3166 if (Option.isNone(oservice)) {
3167 return -1
3168 }
3169 return yield* oservice.value.defaultValue
3170 })
3171 )
3172 )
3173})
3174
3175SchemaParser.makeEffect(schema)({})
3176 .pipe(
3177 Effect.provideService(ConstructorService, ConstructorService.of({ defaultValue: Effect.succeed(0) })),
3178 Effect.runPromise
3179 )
3180 .then(console.log, console.error)
3181// { a: 0 }
3182```
3183
3184# Transformations
3185
3186Transformations convert values from one type to another during decoding or encoding. They are standalone, reusable objects you compose with schemas.
3187
3188## Transformations as First-Class
3189
3190In previous versions, transformations were directly embedded in schemas. In the current version, they are defined as independent values that can be reused across schemas.
3191
3192**Example** (Previous approach: inline transformation)
3193
3194```ts
3195const Trim = transform(
3196 String,
3197 Trimmed,
3198 // non re-usable transformation
3199 {
3200 decode: (i) => i.trim(),
3201 encode: identity
3202 }
3203) {}
3204```
3205
3206This style made it difficult to reuse logic across different schemas.
3207
3208Now, transformations like `trim` are declared once and reused wherever needed.
3209
3210**Example** (The `trim` built-in transformation)
3211
3212```ts
3213import { SchemaTransformation } from "effect"
3214
3215// const t: Transformation<string, string, never, never>
3216const t = SchemaTransformation.trim()
3217```
3218
3219You can apply a transformation to any compatible schema. In this example, `trim` is applied to a string schema using `Schema.decode` (more on this later).
3220
3221**Example** (Applying `trim` to a string schema)
3222
3223```ts
3224import { Schema, SchemaTransformation } from "effect"
3225
3226const schema = Schema.String.pipe(Schema.decode(SchemaTransformation.trim()))
3227
3228console.log(Schema.decodeUnknownSync(schema)(" 123"))
3229// 123
3230```
3231
3232## The Transformation Type
3233
3234A `Transformation` carries four type parameters:
3235
3236```ts
3237Transformation<T, E, RD, RE>
3238```
3239
3240- `T`: the decoded (output) type
3241- `E`: the encoded (input) type
3242- `RD`: the context used while decoding
3243- `RE`: the context used while encoding
3244
3245A `Transformation` consists of two `Getter` values:
3246
3247- `decode: Getter<T, E, RD>` — transforms a value during decoding
3248- `encode: Getter<E, T, RE>` — transforms a value during encoding
3249
3250Each `Getter` is a tagged description of one operation:
3251
3252- `Transform` transforms a present value synchronously.
3253- `TransformOptional` transforms an `Option` synchronously and can handle a missing value.
3254- `TransformEffect` and `TransformOptionalEffect` are the corresponding effectful forms.
3255- `Passthrough` returns its input unchanged.
3256
3257Getter values expose `pipe`. Use the dual standalone functions `SchemaGetter.map` and `SchemaGetter.compose` to build
3258larger transformations. `SchemaGetter.run` executes a getter directly and always returns an `Effect`; schemas execute
3259their getters through `SchemaParser` instead.
3260
3261**Example** (Implementation of `Transformation.trim`)
3262
3263```ts
3264import { SchemaGetter, SchemaTransformation } from "effect"
3265
3266/**
3267 * @category String transformations
3268 * @since 4.0.0
3269 */
3270export function trim(): SchemaTransformation.Transformation<string, string> {
3271 return new SchemaTransformation.Transformation(SchemaGetter.trim(), SchemaGetter.passthrough())
3272}
3273```
3274
3275In this case:
3276
3277- The `decode` process uses `SchemaGetter.trim()` to remove leading and trailing whitespace.
3278- The `encode` process uses `SchemaGetter.passthrough()`, which returns the input as is.
3279
3280## Composing Transformations
3281
3282You can combine transformations using `SchemaTransformation.composeTransformation`. The resulting transformation applies the `decode` and `encode` logic of both transformations in sequence.
3283
3284**Example** (Trim and lowercase a string)
3285
3286```ts
3287import { Schema, SchemaTransformation } from "effect"
3288
3289// Compose two transformations: trim followed by toLowerCase
3290const trimToLowerCase = SchemaTransformation.composeTransformation(
3291 SchemaTransformation.trim(),
3292 SchemaTransformation.toLowerCase()
3293)
3294const schema = Schema.String.pipe(Schema.decode(trimToLowerCase))
3295
3296Schema.decodeUnknownSync(schema)(" Abc")
3297// "abc"
3298```
3299
3300In this example:
3301
3302- The `decode` logic applies `SchemaGetter.trim()` followed by `SchemaGetter.toLowerCase()`, producing a string that is trimmed and lowercased.
3303- The `encode` logic is `SchemaGetter.passthrough()`, which returns the input unchanged.
3304
3305## Transforming One Schema into Another
3306
3307To define how one schema transforms into another, you can use:
3308
3309- `Schema.decodeTo` (and its inverse `Schema.encodeTo`)
3310- `Schema.decode` (and its inverse `Schema.encode`)
3311
3312These functions let you attach transformations to schemas, defining how values should be converted during decoding or encoding.
3313
3314### decodeTo
3315
3316Use `Schema.decodeTo` when you want to transform a source schema into a different target schema.
3317
3318You must provide:
3319
33201. The target schema
33212. An optional transformation
3322
3323If no transformation is provided, the operation is called "schema composition" (see below).
3324
3325**Example** (Parsing a number from a string)
3326
3327```ts
3328import { Schema, SchemaTransformation } from "effect"
3329
3330const NumberFromString =
3331 // source schema: String
3332 Schema.String.pipe(
3333 Schema.decodeTo(
3334 Schema.Number, // target schema: Number
3335 SchemaTransformation.numberFromString // built-in transformation that coerce a string to a number (and back)
3336 )
3337 )
3338
3339console.log(Schema.decodeUnknownSync(NumberFromString)("123"))
3340// 123
3341console.log(Schema.decodeUnknownSync(NumberFromString)("a"))
3342// NaN
3343```
3344
3345### decode
3346
3347Use `Schema.decode` when the source and target schemas are the same and you only want to apply a transformation.
3348
3349This is a shorter version of `decodeTo`.
3350
3351**Example** (Trimming whitespace from a string)
3352
3353```ts
3354import { Schema, SchemaTransformation } from "effect"
3355
3356// Equivalent to decodeTo(Schema.String, Transformation.trim())
3357const TrimmedString = Schema.String.pipe(Schema.decode(SchemaTransformation.trim()))
3358```
3359
3360### Defining an Inline Transformation
3361
3362You can create a transformation directly using helpers from the `SchemaTransformation` module.
3363
3364For example, `SchemaTransformation.transform` lets you define a simple transformation by providing `decode` and `encode` functions.
3365
3366**Example** (Converting meters to kilometers and back)
3367
3368```ts
3369import { Schema, SchemaTransformation } from "effect"
3370
3371// Defines a transformation that converts meters (number) to kilometers (number)
3372// 1000 meters -> 1 kilometer (decode)
3373// 1 kilometer -> 1000 meters (encode)
3374const Kilometers = Schema.Finite.pipe(
3375 Schema.decode(
3376 SchemaTransformation.transform({
3377 decode: (meters) => meters / 1000,
3378 encode: (kilometers) => kilometers * 1000
3379 })
3380 )
3381)
3382```
3383
3384You can define transformations that may fail during decoding or encoding using `SchemaTransformation.transformEffect`.
3385
3386This is useful when you need to validate input or enforce rules that may not always succeed.
3387
3388**Example** (Converting a string URL into a `URL` object)
3389
3390```ts
3391import { Effect, Schema, SchemaIssue, SchemaTransformation } from "effect"
3392
3393const URLFromString = Schema.String.pipe(
3394 Schema.decodeTo(
3395 Schema.instanceOf(URL),
3396 SchemaTransformation.transformEffect({
3397 decode: (s, options) =>
3398 Effect.try({
3399 try: () => new URL(s),
3400 catch: () => new SchemaIssue.InvalidValue({ message: "Invalid URL string" }, s, options)
3401 }),
3402 encode: (url) => Effect.succeed(url.href)
3403 })
3404 )
3405)
3406```
3407
3408## Schema composition
3409
3410You can compose transformations, but you can also compose schemas with `Schema.decodeTo`.
3411
3412**Example** (Converting meters to miles via kilometers)
3413
3414```ts
3415import { Schema, SchemaTransformation } from "effect"
3416
3417const KilometersFromMeters = Schema.Finite.pipe(
3418 Schema.decode(
3419 SchemaTransformation.transform({
3420 decode: (meters) => meters / 1000,
3421 encode: (kilometers) => kilometers * 1000
3422 })
3423 )
3424)
3425
3426const MilesFromKilometers = Schema.Finite.pipe(
3427 Schema.decode(
3428 SchemaTransformation.transform({
3429 decode: (kilometers) => kilometers * 0.621371,
3430 encode: (miles) => miles / 0.621371
3431 })
3432 )
3433)
3434
3435const MilesFromMeters = KilometersFromMeters.pipe(Schema.decodeTo(MilesFromKilometers))
3436```
3437
3438This approach does not require the source and target schemas to be type-compatible. If you need more control over type compatibility, you can use one of the `Transformation.passthrough*` helpers.
3439
3440## Passthrough Helpers
3441
3442The `passthrough`, `passthroughSubtype`, and `passthroughSupertype` helpers let you compose schemas by describing how their types relate.
3443
3444### passthrough
3445
3446Use `passthrough` when the encoded output of the target schema matches the type of the source schema.
3447
3448**Example** (When `To.Encoded === From.Type`)
3449
3450```ts
3451import { Schema, SchemaTransformation } from "effect"
3452
3453const From = Schema.Struct({
3454 a: Schema.String
3455})
3456
3457const To = Schema.Struct({
3458 a: Schema.FiniteFromString
3459})
3460
3461// To.Encoded (string) = From.Type (string)
3462const schema = From.pipe(Schema.decodeTo(To, SchemaTransformation.passthrough()))
3463```
3464
3465### passthroughSubtype
3466
3467Use `passthroughSubtype` when the source type is a subtype of the target's encoded output.
3468
3469**Example** (When `From.Type` is a subtype of `To.Encoded`)
3470
3471```ts
3472import { Schema, SchemaTransformation } from "effect"
3473
3474const From = Schema.FiniteFromString
3475
3476const To = Schema.UndefinedOr(Schema.Number)
3477
3478// From.Type (number) extends To.Encoded (number | undefined)
3479const schema = From.pipe(Schema.decodeTo(To, SchemaTransformation.passthroughSubtype()))
3480```
3481
3482### passthroughSupertype
3483
3484Use `passthroughSupertype` when the target's encoded output is a subtype of the source type.
3485
3486**Example** (When `To.Encoded` is a subtype of `From.Type`)
3487
3488```ts
3489import { Schema, SchemaTransformation } from "effect"
3490
3491const From = Schema.UndefinedOr(Schema.String)
3492
3493const To = Schema.FiniteFromString
3494
3495// To.Encoded (string) extends From.Type (string | undefined)
3496const schema = From.pipe(Schema.decodeTo(To, SchemaTransformation.passthroughSupertype()))
3497```
3498
3499### Turning off strict mode
3500
3501Strict mode ensures that decoding and encoding fully match. You can disable it by passing `{ strict: false }` to `passthrough`.
3502
3503**Example** (Turning off strict mode)
3504
3505```ts
3506import { Schema, SchemaTransformation } from "effect"
3507
3508const From = Schema.Struct({
3509 a: Schema.Literals(["a", "b"]),
3510 b: Schema.Number
3511})
3512
3513const To = Schema.Struct({
3514 a: Schema.String,
3515 b: Schema.Literals([1, 2])
3516})
3517
3518// Neither From.Type nor To.Encoded extends the other.
3519const schema = From.pipe(Schema.decodeTo(To, SchemaTransformation.passthrough({ strict: false })))
3520```
3521
3522## Managing Optional Keys
3523
3524You can control how optional values are handled during transformations using the `SchemaTransformation.transformOptional` helper.
3525
3526This helper works with `Option<E>` and returns an `Option<T>`, where:
3527
3528- `E` is the encoded type
3529- `T` is the decoded type
3530
3531This function is useful when dealing with optional values that may be present or missing during decoding or encoding.
3532
3533If the input is `Option.none()`, it means the value is not provided.
3534If it is `Option.some(value)`, then the transformation logic is applied to `value`.
3535
3536You control the optionality of the output by returning an `Option`:
3537
3538- `Option.none()`: exclude the key from the output
3539- `Option.some(transformedValue)`: include the transformed value
3540
3541**Example** (Optional string key transformed to `Option<NonEmptyString>`)
3542
3543```ts
3544import { Option, Schema, SchemaTransformation } from "effect"
3545
3546const OptionFromNonEmptyString = Schema.optionalKey(Schema.String).pipe(
3547 Schema.decodeTo(
3548 Schema.Option(Schema.NonEmptyString),
3549 SchemaTransformation.transformOptional({
3550 // Convert empty strings to None, and non-empty strings to Some(value)
3551 decode: (oe) =>
3552 Option.isSome(oe) && oe.value !== "" ? Option.some(Option.some(oe.value)) : Option.some(Option.none()),
3553
3554 // Flatten nested Options back to a single optional string
3555 encode: (ot) => Option.flatten(ot)
3556 })
3557 )
3558)
3559
3560const schema = Schema.Struct({
3561 foo: OptionFromNonEmptyString
3562})
3563
3564// Decoding examples
3565
3566console.log(Schema.decodeUnknownSync(schema)({}))
3567// Output: { foo: None }
3568
3569console.log(Schema.decodeUnknownSync(schema)({ foo: "" }))
3570// Output: { foo: None }
3571
3572console.log(Schema.decodeUnknownSync(schema)({ foo: "hi" }))
3573// Output: { foo: Some("hi") }
3574
3575// Encoding examples
3576
3577console.log(Schema.encodeSync(schema)({ foo: Option.none() }))
3578// Output: {}
3579
3580console.log(Schema.encodeSync(schema)({ foo: Option.some("hi") }))
3581// Output: { foo: "hi" }
3582```
3583
3584## Omitting a Key During Encoding
3585
3586Use `SchemaGetter.omit()` to exclude a field from the encoded output. At runtime, `omit()` returns `Option.none()`, which tells the struct parser to skip writing that key.
3587
3588For this to work, the encoded side must be marked as optional with `Schema.optionalKey`. Otherwise, producing `None` for a required field causes a `MissingKey` error.
3589
3590**Example** (Field present when decoded, omitted when encoded)
3591
3592```ts
3593import { Effect, Schema, SchemaGetter } from "effect"
3594
3595const schema = Schema.Struct({
3596 a: Schema.FiniteFromString,
3597 b: Schema.String.pipe(
3598 Schema.encodeTo(Schema.optionalKey(Schema.String), {
3599 decode: SchemaGetter.withDefault(Effect.succeed("default_value")),
3600 encode: SchemaGetter.omit()
3601 })
3602 )
3603})
3604
3605// ┌─── { readonly a: string; readonly b?: string; }
3606// ▼
3607type Encoded = typeof schema.Encoded
3608
3609// ┌─── { readonly a: number; readonly b: string; }
3610// ▼
3611type Type = typeof schema.Type
3612
3613console.log(Schema.decodeUnknownSync(schema)({ a: "1", b: "value" }))
3614// Output: { a: 1, b: "value" }
3615
3616console.log(Schema.decodeUnknownSync(schema)({ a: "1" }))
3617// Output: { a: 1, b: "default_value" }
3618
3619console.log(Schema.encodeSync(schema)({ a: 1, b: "default_value" }))
3620// Output: { a: "1" }
3621```
3622
3623For the common case of a discriminator tag that should be omitted during encoding, use `Schema.tagDefaultOmit`:
3624
3625```ts
3626import { Schema } from "effect"
3627
3628const schema = Schema.Struct({
3629 _tag: Schema.tagDefaultOmit("MyTag"),
3630 a: Schema.FiniteFromString
3631})
3632
3633console.log(Schema.decodeUnknownSync(schema)({ a: "1" }))
3634// Output: { a: 1, _tag: "MyTag" }
3635
3636console.log(Schema.encodeSync(schema)({ a: 1, _tag: "MyTag" }))
3637// Output: { a: "1" }
3638```
3639
3640# Flipping Schemas
3641
3642Flipping a schema swaps its decoding and encoding directions. If a schema decodes a `string` into a `number`, the flipped version decodes a `number` into a `string`. This is useful when you want to reuse an existing schema but invert its direction.
3643
3644**Example** (Flipping a schema that parses a string into a number)
3645
3646```ts
3647import { Schema } from "effect"
3648
3649// Flips a schema that decodes a string into a number,
3650// turning it into one that decodes a number into a string
3651//
3652// ┌─── flip<FiniteFromString>
3653// ▼
3654const StringFromFinite = Schema.flip(Schema.FiniteFromString)
3655```
3656
3657You can access the original schema using the `.schema` property:
3658
3659**Example** (Accessing the original schema)
3660
3661```ts
3662import { Schema } from "effect"
3663
3664const StringFromFinite = Schema.flip(Schema.FiniteFromString)
3665
3666// ┌─── FiniteFromString
3667// ▼
3668StringFromFinite.schema
3669```
3670
3671Flipping a schema twice returns a schema with the same structure and behavior as the original:
3672
3673**Example** (Double flipping restores the original schema)
3674
3675```ts
3676import { Schema } from "effect"
3677
3678// ┌─── FiniteFromString
3679// ▼
3680const schema = Schema.flip(Schema.flip(Schema.FiniteFromString))
3681```
3682
3683## How it works
3684
3685All internal operations in the Schema AST are symmetrical. Encoding with a schema is equivalent to decoding with its flipped version:
3686
3687```ts
3688// Encoding with a schema is the same as decoding with its flipped version
3689encode(schema) = decode(flip(schema))
3690```
3691
3692This symmetry ensures that flipping works consistently across all schema types.
3693
3694## Flipped constructors
3695
3696A flipped schema also includes a constructor. It builds values of the **encoded** type from the original schema.
3697
3698**Example** (Using a flipped schema to construct an encoded value)
3699
3700```ts
3701import { Schema } from "effect"
3702
3703const schema = Schema.Struct({
3704 a: Schema.FiniteFromString
3705})
3706
3707/*
3708type Encoded = {
3709 readonly a: string;
3710}
3711*/
3712type Encoded = (typeof schema)["Encoded"]
3713
3714// make: { readonly a: string } ──▶ { readonly a: string }
3715Schema.flip(schema).make
3716```
3717
3718# Classes and Opaque Types
3719
3720Schema supports two kinds of nominal types: _opaque structs_ for lightweight distinct types, and _classes_ for full-featured types with methods and prototype-backed instances.
3721
3722## Opaque Structs
3723
3724Goal: opaque typing without changing runtime behavior.
3725
3726`Schema.Opaque` lets you take an ordinary `Schema.Struct` and wrap it in a thin class shell whose **only** purpose is to create a distinct TypeScript type.
3727
3728Internally the value is **still the same plain struct schema**.
3729
3730Instance methods and custom constructors **are not allowed** in opaque structs (no `new ...`).
3731This is not enforced at the type level, but it may be enforced through a linter in the future.
3732
3733### How is this different from `Schema.Class`?
3734
3735`Schema.Class` also wraps a `Struct`, **but** it turns the wrapper into a proper class:
3736
3737- You can add instance methods, getters, setters, custom constructors.
3738- Instances compare structurally with `Equal.equals`, but they do not implement `Equal`.
3739- Instances carry the class prototype at runtime, so `instanceof` checks succeed and methods are callable.
3740
3741**Example** (Creating an Opaque Struct)
3742
3743```ts
3744import { Schema } from "effect"
3745
3746class Person extends Schema.Opaque<Person>()(
3747 Schema.Struct({
3748 name: Schema.String
3749 })
3750) {}
3751
3752// ┌─── Codec<Person, { readonly name: string; }, never, never>
3753// ▼
3754const codec = Schema.revealCodec(Person)
3755
3756// const person: Person
3757const person = Person.make({ name: "John" })
3758
3759console.log(person.name)
3760// "John"
3761
3762// The class itself holds the original schema and its metadata
3763console.log(Person)
3764// -> [Function: Person] Struct$
3765
3766// { readonly name: Schema.String }
3767Person.fields
3768
3769/*
3770const another: Schema.Struct<{
3771 readonly name: typeof Person;
3772}>
3773*/
3774const another = Schema.Struct({ name: Person }) // You can use the opaque type inside other schemas
3775
3776/*
3777type Type = {
3778 readonly name: Person;
3779}
3780*/
3781type Type = (typeof another)["Type"]
3782```
3783
3784Opaque structs can be used just like regular structs, with no other changes needed.
3785
3786**Example** (Retrieving Schema Fields)
3787
3788```ts
3789import { Schema } from "effect"
3790
3791// A function that takes a generic struct
3792const getFields = <Fields extends Schema.Struct.Fields>(struct: Schema.Struct<Fields>) => struct.fields
3793
3794class Person extends Schema.Opaque<Person>()(
3795 Schema.Struct({
3796 name: Schema.String
3797 })
3798) {}
3799
3800/*
3801const fields: {
3802 readonly name: Schema.String;
3803}
3804*/
3805const fields = getFields(Person)
3806```
3807
3808### Static methods
3809
3810You can add static members to an opaque struct class to extend its behavior.
3811
3812**Example** (Custom serializer via static method)
3813
3814```ts
3815import { Schema } from "effect"
3816
3817class Person extends Schema.Opaque<Person>()(
3818 Schema.Struct({
3819 name: Schema.String,
3820 createdAt: Schema.Date
3821 })
3822) {
3823 // Create a custom serializer using the class itself
3824 static readonly serializer = Schema.toCodecJson(this)
3825}
3826
3827console.log(
3828 Schema.encodeUnknownSync(Person)({
3829 name: "John",
3830 createdAt: new Date()
3831 })
3832)
3833// { name: 'John', createdAt: 2025-05-02T13:49:29.926Z }
3834
3835console.log(
3836 Schema.encodeUnknownSync(Person.serializer)({
3837 name: "John",
3838 createdAt: new Date()
3839 })
3840)
3841// { name: 'John', createdAt: '2025-05-02T13:49:29.928Z' }
3842```
3843
3844### Annotations and filters
3845
3846You can attach filters and annotations to the struct passed into `Opaque`.
3847
3848**Example** (Applying a filter and title annotation)
3849
3850```ts
3851import { Schema } from "effect"
3852
3853class Person extends Schema.Opaque<Person>()(
3854 Schema.Struct({
3855 name: Schema.String
3856 }).annotate({ identifier: "Person" })
3857) {}
3858
3859console.log(String(Schema.decodeUnknownExit(Person)(null)))
3860// Failure(Cause([Fail(SchemaError: Expected Person)]))
3861```
3862
3863When you call methods like `annotate` on an opaque struct, you get back the original struct, not a new class.
3864
3865```ts
3866import { Schema } from "effect"
3867
3868class Person extends Schema.Opaque<Person>()(
3869 Schema.Struct({
3870 name: Schema.String
3871 })
3872) {}
3873
3874/*
3875const S: Schema.Struct<{
3876 readonly name: Schema.String;
3877}>
3878*/
3879const S = Person.annotate({ title: "Person" }) // `annotate` returns the wrapped struct type
3880```
3881
3882### Recursive Opaque Structs
3883
3884**Example** (Recursive Opaque Struct with Same Encoded and Type)
3885
3886```ts
3887import { Schema } from "effect"
3888
3889export class Category extends Schema.Opaque<Category>()(
3890 Schema.Struct({
3891 name: Schema.String,
3892 children: Schema.Array(Schema.suspend((): Schema.Codec<Category> => Category))
3893 })
3894) {}
3895
3896/*
3897type Encoded = {
3898 readonly children: readonly Category[];
3899 readonly name: string;
3900}
3901*/
3902export type Encoded = (typeof Category)["Encoded"]
3903```
3904
3905**Example** (Recursive Opaque Struct with Different Encoded and Type)
3906
3907```ts
3908import { Schema } from "effect"
3909
3910interface CategoryEncoded extends Schema.Codec.Encoded<typeof Category> {}
3911
3912export class Category extends Schema.Opaque<Category>()(
3913 Schema.Struct({
3914 name: Schema.FiniteFromString,
3915 children: Schema.Array(Schema.suspend((): Schema.Codec<Category, CategoryEncoded> => Category))
3916 })
3917) {}
3918
3919/*
3920type Encoded = {
3921 readonly children: readonly CategoryEncoded[];
3922 readonly name: string;
3923}
3924*/
3925export type Encoded = (typeof Category)["Encoded"]
3926```
3927
3928**Example** (Mutually Recursive Schemas)
3929
3930```ts
3931import { Schema } from "effect"
3932
3933class Expression extends Schema.Opaque<Expression>()(
3934 Schema.Struct({
3935 type: Schema.Literal("expression"),
3936 value: Schema.Union([Schema.Number, Schema.suspend((): Schema.Codec<Operation> => Operation)])
3937 })
3938) {}
3939
3940class Operation extends Schema.Opaque<Operation>()(
3941 Schema.Struct({
3942 type: Schema.Literal("operation"),
3943 operator: Schema.Literals(["+", "-"]),
3944 left: Expression,
3945 right: Expression
3946 })
3947) {}
3948
3949/*
3950type Encoded = {
3951 readonly type: "operation";
3952 readonly operator: "+" | "-";
3953 readonly left: {
3954 readonly type: "expression";
3955 readonly value: number | Operation;
3956 };
3957 readonly right: {
3958 readonly type: "expression";
3959 readonly value: number | Operation;
3960 };
3961}
3962*/
3963export type Encoded = (typeof Operation)["Encoded"]
3964```
3965
3966### Branded Opaque Structs
3967
3968You can brand an opaque struct using the `Brand` generic parameter.
3969
3970**Example** (Branded Opaque Struct)
3971
3972```ts
3973import { Schema } from "effect"
3974
3975class A extends Schema.Opaque<A, { readonly brand: unique symbol }>()(
3976 Schema.Struct({
3977 a: Schema.String
3978 })
3979) {}
3980class B extends Schema.Opaque<B, { readonly brand: unique symbol }>()(
3981 Schema.Struct({
3982 a: Schema.String
3983 })
3984) {}
3985
3986const f = (a: A) => a
3987const g = (b: B) => b
3988
3989f(A.make({ a: "a" })) // ok
3990g(B.make({ a: "a" })) // ok
3991
3992f(B.make({ a: "a" })) // error: Argument of type 'B' is not assignable to parameter of type 'A'.
3993g(A.make({ a: "a" })) // error: Argument of type 'A' is not assignable to parameter of type 'B'.
3994```
3995
3996Like with branded classes, you can use the `Brand` module to create branded opaque structs.
3997
3998```ts
3999import { Schema } from "effect"
4000import type { Brand } from "effect"
4001
4002class A extends Schema.Opaque<A, Brand.Brand<"A">>()(
4003 Schema.Struct({
4004 a: Schema.String
4005 })
4006) {}
4007class B extends Schema.Opaque<B, Brand.Brand<"B">>()(
4008 Schema.Struct({
4009 a: Schema.String
4010 })
4011) {}
4012
4013const f = (a: A) => a
4014const g = (b: B) => b
4015
4016f(A.make({ a: "a" })) // ok
4017g(B.make({ a: "a" })) // ok
4018
4019f(B.make({ a: "a" })) // error: Argument of type 'B' is not assignable to parameter of type 'A'.
4020g(A.make({ a: "a" })) // error: Argument of type 'A' is not assignable to parameter of type 'B'.
4021```
4022
4023## Schema as a Class
4024
4025Any schema can be extended directly with `extends`. The resulting class inherits the full schema API (e.g. `annotate`) and supports static methods that reference `this`.
4026
4027Unlike `Schema.Opaque`, it does **not** make the decoded type nominally distinct, and unlike `Schema.Class`, it does **not** create prototype-backed instances with methods or constructors. It is a lightweight way to attach custom static helpers to a schema.
4028
4029### Wrapping a Primitive Schema
4030
4031```ts
4032import { Schema } from "effect"
4033
4034class MyString extends Schema.String {
4035 static readonly decodeUnknownSync = Schema.decodeUnknownSync(this)
4036}
4037
4038console.log(MyString.decodeUnknownSync("a"))
4039// "a"
4040```
4041
4042### Wrapping a Struct Schema
4043
4044```ts
4045import { Schema } from "effect"
4046
4047class MyStruct extends Schema.Struct({ name: Schema.String }) {
4048 static readonly decodeUnknownSync = Schema.decodeUnknownSync(this)
4049}
4050
4051console.log(MyStruct.decodeUnknownSync({ name: "a" }))
4052// { name: "a" }
4053```
4054
4055### Subclassing
4056
4057You can extend a schema class to layer on more static helpers:
4058
4059```ts
4060import { Schema } from "effect"
4061
4062class MyString extends Schema.FiniteFromString {
4063 static readonly decodeUnknownSync = Schema.decodeUnknownSync(this)
4064}
4065
4066class MyString2 extends MyString {
4067 static readonly encodeSync = Schema.encodeSync(this)
4068}
4069
4070console.log(MyString2.decodeUnknownSync("1"))
4071// 1
4072console.log(MyString2.encodeSync(1))
4073// "1"
4074```
4075
4076## Classes
4077
4078### Existing Classes
4079
4080#### Validating the Constructor
4081
4082**Use Case**: When you want to validate the constructor arguments of an existing class.
4083
4084**Example** (Using a tuple to validate the constructor arguments)
4085
4086```ts
4087import { Schema } from "effect"
4088
4089const PersonConstructorArguments = Schema.Tuple([Schema.String, Schema.Finite])
4090
4091// Existing class
4092class Person {
4093 constructor(readonly name: string, readonly age: number) {
4094 PersonConstructorArguments.make([name, age])
4095 }
4096}
4097
4098try {
4099 new Person("John", NaN)
4100} catch (error) {
4101 if (error instanceof Error) {
4102 console.log(error.message)
4103 }
4104}
4105/*
4106Expected a finite number
4107 at [1]
4108*/
4109```
4110
4111**Example** (Inheritance)
4112
4113```ts
4114import { Schema } from "effect"
4115
4116const PersonConstructorArguments = Schema.Tuple([Schema.String, Schema.Finite])
4117
4118class Person {
4119 constructor(readonly name: string, readonly age: number) {
4120 PersonConstructorArguments.make([name, age])
4121 }
4122}
4123
4124const PersonWithEmailConstructorArguments = Schema.Tuple([Schema.String])
4125
4126class PersonWithEmail extends Person {
4127 constructor(name: string, age: number, readonly email: string) {
4128 // Only validate the additional argument
4129 PersonWithEmailConstructorArguments.make([email])
4130 super(name, age)
4131 }
4132}
4133```
4134
4135#### Defining a Schema
4136
4137```ts
4138import { Schema, SchemaTransformation } from "effect"
4139
4140class Person {
4141 constructor(readonly name: string, readonly age: number) {}
4142}
4143
4144const PersonSchema = Schema.instanceOf(Person, {
4145 title: "Person",
4146 // optional: default JSON serialization
4147 toCodecJson: () =>
4148 Schema.link<Person>()(
4149 Schema.Tuple([Schema.String, Schema.Number]),
4150 SchemaTransformation.transform({
4151 decode: (args) => new Person(...args),
4152 encode: (instance) => [instance.name, instance.age] as const
4153 })
4154 )
4155})
4156 // optional: explicit encoding
4157 .pipe(
4158 Schema.encodeTo(
4159 Schema.Struct({
4160 name: Schema.String,
4161 age: Schema.Number
4162 }),
4163 SchemaTransformation.transform({
4164 decode: (args) => new Person(args.name, args.age),
4165 encode: (instance) => instance
4166 })
4167 )
4168 )
4169```
4170
4171**Example** (Inheritance)
4172
4173```ts
4174import { Schema, SchemaTransformation } from "effect"
4175
4176class Person {
4177 constructor(readonly name: string, readonly age: number) {}
4178}
4179
4180const PersonSchema = Schema.instanceOf(Person, {
4181 title: "Person",
4182 // optional: default JSON serialization
4183 toCodecJson: () =>
4184 Schema.link<Person>()(
4185 Schema.Tuple([Schema.String, Schema.Number]),
4186 SchemaTransformation.transform({
4187 decode: (args) => new Person(...args),
4188 encode: (instance) => [instance.name, instance.age] as const
4189 })
4190 )
4191})
4192 // optional: explicit encoding
4193 .pipe(
4194 Schema.encodeTo(
4195 Schema.Struct({
4196 name: Schema.String,
4197 age: Schema.Number
4198 }),
4199 SchemaTransformation.transform({
4200 decode: (args) => new Person(args.name, args.age),
4201 encode: (instance) => instance
4202 })
4203 )
4204 )
4205
4206class PersonWithEmail extends Person {
4207 constructor(name: string, age: number, readonly email: string) {
4208 super(name, age)
4209 }
4210}
4211
4212// const PersonWithEmailSchema = ...repeat the pattern above...
4213```
4214
4215#### Errors
4216
4217**Example** (Extending Data.Error)
4218
4219```ts
4220import { Data, Effect, identity, Schema, SchemaTransformation } from "effect"
4221
4222const Props = Schema.Struct({
4223 message: Schema.String
4224})
4225
4226class Err extends Data.Error<typeof Props.Type> {
4227 constructor(props: typeof Props.Type) {
4228 super(Props.make(props))
4229 }
4230}
4231
4232const program = Effect.gen(function*() {
4233 yield* new Err({ message: "Uh oh" })
4234})
4235
4236Effect.runPromiseExit(program).then((exit) => console.log(JSON.stringify(exit, null, 2)))
4237/*
4238{
4239 "_id": "Exit",
4240 "_tag": "Failure",
4241 "cause": {
4242 "_id": "Cause",
4243 "failures": [
4244 {
4245 "_tag": "Fail",
4246 "error": {
4247 "message": "Uh oh"
4248 }
4249 }
4250 ]
4251 }
4252}
4253*/
4254
4255const transformation = SchemaTransformation.transform<Err, (typeof Props)["Type"]>({
4256 decode: (props) => new Err(props),
4257 encode: identity
4258})
4259
4260const schema = Schema.instanceOf(Err, {
4261 title: "Err",
4262 serialization: {
4263 json: () => Schema.link<Err>()(Props, transformation)
4264 }
4265}).pipe(Schema.encodeTo(Props, transformation))
4266```
4267
4268### Class API
4269
4270**Example** (Constructing and decoding a class)
4271
4272```ts
4273import { Schema } from "effect"
4274
4275// Define a class with a single string field "a"
4276class A extends Schema.Class<A>("A")({
4277 a: Schema.String
4278}) {
4279 // Regular class fields are allowed
4280 readonly _a = 1
4281}
4282
4283console.log(new A({ a: "a" }))
4284// A { a: 'a', _a: 1 }
4285console.log(A.make({ a: "a" }))
4286// A { a: 'a', _a: 1 }
4287console.log(Schema.decodeUnknownSync(A)({ a: "a" }))
4288// A { a: 'a', _a: 1 }
4289```
4290
4291#### Filters
4292
4293To attach a filter to the whole class, pass a `Struct` instead of a field record and call `.check(...)` on it.
4294
4295**Example** (Validating a relationship between fields)
4296
4297```ts
4298import { Schema } from "effect"
4299
4300class A extends Schema.Class<A>("A")(
4301 Schema.Struct({
4302 a: Schema.String,
4303 b: Schema.String
4304 }).check(Schema.makeFilter(({ a, b }) => a === b, { title: "a === b" }))
4305) {}
4306
4307try {
4308 new A({ a: "a", b: "b" })
4309} catch (error: any) {
4310 console.log(error.message)
4311}
4312// Expected a === b
4313
4314try {
4315 Schema.decodeUnknownSync(A)({ a: "a", b: "b" })
4316} catch (error: any) {
4317 console.log(error.message)
4318}
4319// Expected a === b
4320```
4321
4322#### Branded Classes
4323
4324Attach a brand to a class to avoid mixing values from different domains that share the same structure.
4325
4326**Example** (Unique brands block assignment)
4327
4328```ts
4329import { Schema } from "effect"
4330
4331// Brand the class using a unique symbol type parameter
4332class A extends Schema.Class<A, { readonly brand: unique symbol }>("A")({
4333 a: Schema.String
4334}) {}
4335
4336class B extends Schema.Class<B, { readonly brand: unique symbol }>("B")({
4337 a: Schema.String
4338}) {}
4339
4340// Even though A and B have the same fields, their brands are different,
4341// so they are not assignable to each other.
4342
4343// @ts-expect-error
4344export const a: A = B.make({ a: "a" })
4345// @ts-expect-error
4346export const b: B = A.make({ a: "a" })
4347```
4348
4349**Example** (Using the Brand module)
4350
4351```ts
4352import type { Brand } from "effect"
4353import { Schema } from "effect"
4354
4355class A extends Schema.Class<A, Brand.Brand<"A">>("A")({
4356 a: Schema.String
4357}) {}
4358
4359class B extends Schema.Class<B, Brand.Brand<"B">>("B")({
4360 a: Schema.String
4361}) {}
4362
4363// Different named brands are still not assignable
4364
4365// @ts-expect-error
4366export const a: A = B.make({ a: "a" })
4367// @ts-expect-error
4368export const b: B = A.make({ a: "a" })
4369```
4370
4371#### Annotations
4372
4373Attach metadata to a class schema. The metadata is stored as annotations on the schema AST and can be read at runtime.
4374
4375**Example** (Attaching and reading annotations)
4376
4377```ts
4378import { Schema } from "effect"
4379
4380export class A extends Schema.Class<A>("A")(
4381 {
4382 a: Schema.String
4383 },
4384 // Attach metadata (e.g., title) alongside the schema
4385 { title: "my title" }
4386) {}
4387
4388console.log(A.ast.annotations?.title)
4389// "my title"
4390```
4391
4392#### extend
4393
4394Use `extend` to create a subclass that adds fields to the base schema. Instance fields declared on the base class are also available on the subclass.
4395
4396**Example** (Extending a class with new fields)
4397
4398```ts
4399import { Schema } from "effect"
4400
4401// Base class with one schema field ("a") and one regular class field ("_a")
4402class A extends Schema.Class<A>("A")(
4403 Schema.Struct({
4404 a: Schema.String
4405 })
4406) {
4407 readonly _a = 1
4408}
4409
4410// Subclass adds a new schema field ("b") and its own regular field ("_b")
4411class B extends A.extend<B>("B")({
4412 b: Schema.Number
4413}) {
4414 readonly _b = 2
4415}
4416
4417console.log(new B({ a: "a", b: 2 }))
4418// B { a: 'a', _a: 1, _b: 2 }
4419console.log(B.make({ a: "a", b: 2 }))
4420// B { a: 'a', _a: 1, _b: 2 }
4421console.log(Schema.decodeUnknownSync(B)({ a: "a", b: 2 }))
4422// B { a: 'a', _a: 1, _b: 2 }
4423```
4424
4425#### extends and static members
4426
4427To keep static members from the base class, pass `typeof Base` as the second generic parameter when calling `extend`.
4428
4429**Example** (Preserving static members on subclasses)
4430
4431```ts
4432import { Schema } from "effect"
4433
4434class A extends Schema.Class<A>("A")({
4435 a: Schema.String
4436}) {
4437 static readonly foo = "foo"
4438}
4439
4440class B extends A.extend<B, typeof A>("B")({
4441 b: Schema.Number
4442}) {}
4443
4444console.log(B.foo)
4445// "foo"
4446```
4447
4448#### Recursive Classes
4449
4450Use `Schema.suspend` to reference a class inside its own definition. This is common for tree-like data structures.
4451
4452**Example** (Self-referential tree structure)
4453
4454```ts
4455import { Schema } from "effect"
4456
4457// A simple tree of categories where each node can have child categories.
4458// Use Schema.suspend to refer to Category while it is being defined.
4459export class Category extends Schema.Class<Category>("Category")(
4460 Schema.Struct({
4461 name: Schema.String,
4462 children: Schema.Array(Schema.suspend((): Schema.Codec<Category> => Category))
4463 })
4464) {}
4465
4466/*
4467type Encoded = {
4468 readonly children: readonly Category[];
4469 readonly name: string;
4470}
4471*/
4472export type Encoded = (typeof Category)["Encoded"]
4473```
4474
4475**Example** (Recursive schema with different Encoded and Type)
4476
4477```ts
4478import { Schema } from "effect"
4479
4480// Define the encoded representation for Category separately.
4481// This is useful when the Encoded type differs from the Type type.
4482interface CategoryEncoded extends Schema.Codec.Encoded<typeof Category> {}
4483
4484// The runtime type is Category; the encoded form is CategoryEncoded.
4485// "name" is decoded from a string to a finite number to show that
4486// Type and Encoded types can differ.
4487export class Category extends Schema.Class<Category>("Category")(
4488 Schema.Struct({
4489 name: Schema.FiniteFromString,
4490 children: Schema.Array(Schema.suspend((): Schema.Codec<Category, CategoryEncoded> => Category))
4491 })
4492) {}
4493
4494/*
4495type Encoded = {
4496 readonly children: readonly CategoryEncoded[];
4497 readonly name: string;
4498}
4499*/
4500export type Encoded = (typeof Category)["Encoded"]
4501```
4502
4503**Example** (Mutually recursive expression language)
4504
4505```ts
4506import { Schema } from "effect"
4507
4508class Expression extends Schema.Class<Expression>("Expression")(
4509 Schema.Struct({
4510 type: Schema.Literal("expression"),
4511 value: Schema.Union([Schema.Number, Schema.suspend((): Schema.Codec<Operation> => Operation)])
4512 })
4513) {}
4514
4515class Operation extends Schema.Class<Operation>("Operation")(
4516 Schema.Struct({
4517 type: Schema.Literal("operation"),
4518 operator: Schema.Literals(["+", "-"]),
4519 left: Expression,
4520 right: Expression
4521 })
4522) {}
4523
4524/*
4525type Encoded = {
4526 readonly type: "operation";
4527 readonly operator: "+" | "-";
4528 readonly left: {
4529 readonly type: "expression";
4530 readonly value: number | Operation;
4531 };
4532 readonly right: {
4533 readonly type: "expression";
4534 readonly value: number | Operation;
4535 };
4536}
4537*/
4538export type Encoded = (typeof Operation)["Encoded"]
4539```
4540
4541### TaggedClass
4542
4543`TaggedClass` is a convenience over `Class` that automatically adds a `_tag` field using `Schema.tag`. This is useful for discriminated unions where each variant needs a tag.
4544
4545The tag value doubles as the identifier by default. Pass an explicit identifier as the first argument to override it.
4546
4547**Example** (Basic tagged class)
4548
4549```ts
4550import { Schema } from "effect"
4551
4552class Person extends Schema.TaggedClass<Person>()("Person", {
4553 name: Schema.String
4554}) {}
4555
4556const mike = new Person({ name: "Mike" })
4557console.log(mike)
4558// Person { _tag: 'Person', name: 'Mike' }
4559console.log(mike._tag)
4560// "Person"
4561```
4562
4563**Example** (Custom identifier)
4564
4565```ts
4566import { Schema } from "effect"
4567
4568class Person extends Schema.TaggedClass<Person>("MyPerson")("Person", {
4569 name: Schema.String
4570}) {}
4571
4572console.log(Person.identifier)
4573// "MyPerson"
4574console.log(new Person({ name: "Mike" })._tag)
4575// "Person"
4576```
4577
4578**Example** (Discriminated union)
4579
4580```ts
4581import { Schema } from "effect"
4582
4583class Cat extends Schema.TaggedClass<Cat>()("Cat", {
4584 lives: Schema.Number
4585}) {}
4586
4587class Dog extends Schema.TaggedClass<Dog>()("Dog", {
4588 wagsTail: Schema.Boolean
4589}) {}
4590
4591const Animal = Schema.Union([Cat, Dog])
4592
4593console.log(Schema.decodeUnknownSync(Animal)({ _tag: "Cat", lives: 9 }))
4594// Cat { _tag: 'Cat', lives: 9 }
4595```
4596
4597All features from `Class` are available: `extend`, `annotate`, `check`, branded classes, and recursive definitions.
4598
4599### Error
4600
4601```ts
4602import { Schema } from "effect"
4603
4604class E extends Schema.Error<E>("E")({
4605 id: Schema.Number
4606}) {}
4607```
4608
4609### TaggedError
4610
4611`TaggedError` combines `Error` with an automatic `_tag` field, giving you a tagged error that can be caught with `Effect.catchTag`.
4612
4613Like `TaggedClass`, the tag value doubles as the identifier by default, and you can pass an explicit identifier as the first argument to override it.
4614
4615**Example** (Defining and catching a tagged error)
4616
4617```ts
4618import { Effect, Schema } from "effect"
4619
4620class HttpError extends Schema.TaggedError<HttpError>()("HttpError", {
4621 status: Schema.Number,
4622 message: Schema.String
4623}) {}
4624
4625const program = Effect.gen(function*() {
4626 yield* new HttpError({ status: 404, message: "Not found" })
4627})
4628
4629const recovered = program.pipe(
4630 Effect.catchTag("HttpError", (err) => Effect.succeed(`Caught: ${err.status} ${err.message}`))
4631)
4632```
4633
4634**Example** (Multiple tagged errors in a union)
4635
4636```ts
4637import { Effect, Schema } from "effect"
4638
4639class NotFound extends Schema.TaggedError<NotFound>()("NotFound", {
4640 path: Schema.String
4641}) {}
4642
4643class Unauthorized extends Schema.TaggedError<Unauthorized>()("Unauthorized", {
4644 reason: Schema.String
4645}) {}
4646
4647const program = Effect.gen(function*() {
4648 if (Math.random() < 0.5) {
4649 yield* new Unauthorized({ reason: "Unauthorized" })
4650 } else {
4651 yield* new NotFound({ path: "/missing" })
4652 }
4653})
4654
4655// Each error can be caught independently by its tag
4656const recovered = program.pipe(
4657 Effect.catchTags({
4658 NotFound: (err) => Effect.succeed(`Not found: ${err.path}`),
4659 Unauthorized: (err) => Effect.succeed(`Unauthorized: ${err.reason}`)
4660 })
4661)
4662```
4663
4664All features from `Error` are available: `extend`, `annotate`, and `check`.
4665
4666# Serialization
4667
4668Serialization converts typed values into a format suitable for storage or transmission (such as JSON, FormData, or XML). Deserialization reverses the process, turning raw data back into typed values. Schema provides built-in support for several common formats.
4669
4670## JSON Support
4671
4672#### fromJsonString
4673
4674Returns a schema that decodes a JSON string and then decodes the parsed value using the given schema.
4675
4676Use `Schema.Unknown` to parse the JSON string without validating its structure, or provide a more specific schema to validate the parsed value.
4677
4678Decoding fails if the input is not valid JSON or the parsed value does not satisfy the provided schema.
4679
4680During encoding, the schema encodes the value with the provided schema and then converts the result to a JSON string.
4681
4682**Example** (Parsing without validation)
4683
4684```ts
4685import { Schema } from "effect"
4686
4687Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(`{"a":1,"b":2}`)
4688// => { a: 1, b: 2 }
4689```
4690
4691**Example** (Parsing and validating a known structure)
4692
4693```ts
4694import { Schema } from "effect"
4695
4696const schema = Schema.Struct({ a: Schema.Number })
4697const schemaFromJsonString = Schema.fromJsonString(schema)
4698
4699Schema.decodeUnknownSync(schemaFromJsonString)(`{"a":1,"b":2}`)
4700// => { a: 1 }
4701```
4702
4703## String Encoding Support
4704
4705Schema provides built-in schemas for common string encodings. Each one decodes an encoded string into a UTF-8 string (and encodes back). They can be composed with `fromJsonString` to decode structured data in a single pipeline.
4706
4707#### StringFromBase64
4708
4709Decodes a Base64-encoded (RFC 4648) string into a UTF-8 string.
4710
4711```ts
4712import { Schema } from "effect"
4713
4714Schema.decodeUnknownSync(Schema.StringFromBase64)("aGVsbG8=")
4715// => "hello"
4716```
4717
4718Compose with `fromJsonString` to decode Base64-encoded JSON into a validated struct:
4719
4720```ts
4721import { Schema } from "effect"
4722
4723const schema = Schema.Struct({ a: Schema.Number })
4724
4725// base64 string -> UTF-8 string -> parsed & validated struct
4726const schemaFromBase64 = Schema.StringFromBase64.pipe(
4727 Schema.decodeTo(Schema.fromJsonString(schema))
4728)
4729```
4730
4731#### StringFromBase64Url
4732
4733Like `StringFromBase64`, but uses the URL-safe Base64 alphabet (RFC 4648 section 5).
4734
4735```ts
4736import { Schema } from "effect"
4737
4738Schema.decodeUnknownSync(Schema.StringFromBase64Url)("aGVsbG8")
4739// => "hello"
4740```
4741
4742#### StringFromHex
4743
4744Decodes a hex-encoded string into a UTF-8 string.
4745
4746```ts
4747import { Schema } from "effect"
4748
4749Schema.decodeUnknownSync(Schema.StringFromHex)("68656c6c6f")
4750// => "hello"
4751```
4752
4753#### StringFromUriComponent
4754
4755Decodes a URI-component-encoded string into a UTF-8 string. Useful for storing structured data in URL query parameters.
4756
4757```ts
4758import { Schema } from "effect"
4759
4760const PaginationSchema = Schema.Struct({
4761 maxItemPerPage: Schema.Number,
4762 page: Schema.Number
4763})
4764
4765const UrlSchema = Schema.StringFromUriComponent.pipe(
4766 Schema.decodeTo(Schema.fromJsonString(PaginationSchema))
4767)
4768
4769console.log(Schema.encodeSync(UrlSchema)({ maxItemPerPage: 10, page: 1 }))
4770// %7B%22maxItemPerPage%22%3A10%2C%22page%22%3A1%7D
4771```
4772
4773#### Uint8Array variants
4774
4775For binary data, use the `Uint8Array` variants instead:
4776
4777- `Schema.Uint8ArrayFromBase64` - decodes Base64 into a `Uint8Array`.
4778- `Schema.Uint8ArrayFromBase64Url` - decodes URL-safe Base64 into a `Uint8Array`.
4779- `Schema.Uint8ArrayFromHex` - decodes hex into a `Uint8Array`.
4780
4781#### Low-level transformations
4782
4783The `SchemaTransformation` module exposes the underlying transformations (`stringFromBase64String`, `stringFromBase64UrlString`, `stringFromHexString`, `stringFromUriComponent`). Prefer the built-in `Schema.*` schemas above unless you need to build a custom pipeline.
4784
4785## FormData Support
4786
4787`Schema.fromFormData` returns a schema that reads a `FormData` instance,
4788converts it into a tree record using bracket notation, and then decodes the
4789resulting structure using the provided schema.
4790
4791The decoding process has two steps:
4792
47931. Parse `FormData` into a nested tree record.
47942. Decode the parsed value with the given schema.
4795
4796**Example** (Decoding a flat structure)
4797
4798```ts
4799import { Schema } from "effect"
4800
4801const schema = Schema.fromFormData(
4802 Schema.Struct({
4803 a: Schema.String
4804 })
4805)
4806
4807const formData = new FormData()
4808formData.append("a", "1")
4809formData.append("b", "2")
4810
4811console.log(String(Schema.decodeUnknownExit(schema)(formData)))
4812// Success({"a":"1"})
4813```
4814
4815You can express nested values using bracket notation.
4816
4817**Example** (Nested fields)
4818
4819```ts
4820import { Schema } from "effect"
4821
4822const schema = Schema.fromFormData(
4823 Schema.Struct({
4824 a: Schema.String,
4825 b: Schema.Struct({
4826 c: Schema.String,
4827 d: Schema.String
4828 })
4829 })
4830)
4831
4832const formData = new FormData()
4833formData.append("a", "1")
4834formData.append("b[c]", "2")
4835formData.append("b[d]", "3")
4836
4837console.log(String(Schema.decodeUnknownExit(schema)(formData)))
4838// Success({"a":"1","b":{"c":"2","d":"3"}})
4839```
4840
4841If you want to decode string fields into non-string primitive values, use `Schema.toCodecStringTree`.
4842
4843**Example** (Parsing non-string values)
4844
4845```ts
4846import { Schema } from "effect"
4847
4848const schema = Schema.fromFormData(
4849 Schema.toCodecStringTree(
4850 Schema.Struct({
4851 a: Schema.Int
4852 })
4853 )
4854)
4855
4856const formData = new FormData()
4857formData.append("a", "1")
4858
4859console.log(String(Schema.decodeUnknownExit(schema)(formData)))
4860// Success({"a":1}) // Note: the value is a number
4861```
4862
4863## URLSearchParams Support
4864
4865`Schema.fromURLSearchParams` returns a schema that reads a `URLSearchParams`
4866instance, converts it into a tree record using bracket notation, and then decodes
4867the resulting structure using the provided schema.
4868
4869The decoding process has two steps:
4870
48711. Parse `URLSearchParams` into a nested tree record.
48722. Decode the parsed value with the given schema.
4873
4874**Example** (Decoding a flat structure)
4875
4876```ts
4877import { Schema } from "effect"
4878
4879const schema = Schema.fromURLSearchParams(
4880 Schema.Struct({
4881 a: Schema.String
4882 })
4883)
4884
4885const urlSearchParams = new URLSearchParams("a=1&b=2")
4886
4887console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams)))
4888// Success({"a":"1"})
4889```
4890
4891You can express nested values using bracket notation.
4892
4893**Example** (Nested fields)
4894
4895```ts
4896import { Schema } from "effect"
4897
4898const schema = Schema.fromURLSearchParams(
4899 Schema.Struct({
4900 a: Schema.String,
4901 b: Schema.Struct({
4902 c: Schema.String,
4903 d: Schema.String
4904 })
4905 })
4906)
4907
4908const urlSearchParams = new URLSearchParams("a=1&b[c]=2&b[d]=3")
4909
4910console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams)))
4911// Success({"a":"1","b":{"c":"2","d":"3"}})
4912```
4913
4914If you want to decode values that are not strings, use `Schema.toCodecStringTree`. This serializer preserves values such as numbers when compatible with the schema.
4915
4916**Example** (Parsing non-string values)
4917
4918```ts
4919import { Schema } from "effect"
4920
4921const schema = Schema.fromURLSearchParams(
4922 Schema.toCodecStringTree(
4923 Schema.Struct({
4924 a: Schema.Int
4925 })
4926 )
4927)
4928
4929const urlSearchParams = new URLSearchParams("a=1&b=2")
4930
4931console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams)))
4932// Success({"a":1}) // Note: the value is a number
4933```
4934
4935## Canonical Codecs
4936
4937When sending data over the network or storing it on disk, you need to convert your domain types to a format like JSON. Schema provides built-in support for serializing values to JSON, strings, FormData, URLSearchParams, and XML.
4938
4939Canonical codecs turn one schema into another schema (a "codec") that can serialize and deserialize values using a specific format (JSON, strings, `URLSearchParams`, `FormData`, and so on). This helps you map your domain types to formats that can only represent a limited set of values.
4940
4941To keep things concrete, the rest of this page focuses on JSON.
4942
4943### JSON Canonical Codec
4944
4945Many JavaScript values cannot be serialized to JSON in a safe and reversible way:
4946
4947- `Date`: `JSON.stringify()` converts a date to an ISO string, but `JSON.parse()` does not restore a `Date` object
4948- `Uint8Array`, `ReadonlyMap`, `ReadonlySet`: `JSON.stringify()` converts them to `{}`, so the original data is lost
4949- `Symbol`, `BigInt`: `JSON.stringify()` throws errors
4950- Custom classes and Effect data types (`Option`, `Result`, and so on): `JSON.stringify()` does not know how to encode or decode them
4951
4952This can lead to data loss, runtime errors, or values that decode into the wrong shape when you try to round-trip complex data through JSON.
4953
4954**The solution**
4955
4956A canonical codec describes how values that match a schema should be converted to a specific format. In practice, canonical codecs work like this:
4957
49581. **Annotation-based**: you choose a serialization strategy by adding annotations to your schema (for example `toCodecJson`, `toCodecIso`, `toCodecStringTree`, and others).
49592. **AST transformation**: the codec builder walks the schema AST and produces a new schema that represents the serialized form (this traversal is handled by Effect).
49603. **Recursive composition**: codecs apply through nested structures (objects, arrays, unions, and so on) without you having to wire everything manually.
4961
4962Canonical codec derivation does not run transformations; the consuming parser chooses execution and failure handling. Annotation links may be asynchronous, may fail, and may use optional services, but cannot add required services absent from the derived codec type. `toCodecIso` declares none, while `toCodecJson` and `toCodecStringTree` preserve the input schema's requirements.
4963
4964The next example shows why a custom class needs a codec when working with JSON.
4965
4966**Example** (A custom class that does not round-trip through JSON)
4967
4968```ts
4969import { Schema } from "effect"
4970
4971class Point {
4972 constructor(public readonly x: number, public readonly y: number) {}
4973
4974 // Plain method on a class instance
4975 distance(other: Point): number {
4976 const dx = this.x - other.x
4977 const dy = this.y - other.y
4978 return Math.sqrt(dx * dx + dy * dy)
4979 }
4980}
4981
4982const PointSchema = Schema.instanceOf(Point)
4983```
4984
4985Without an annotation, `Schema.toCodecJson` uses `Schema.Json` as the encoded representation. A `Point` instance is not a JSON value, and a plain object parsed from JSON is not a `Point` instance (including its prototype and methods), so neither direction can bridge the two representations.
4986
4987```ts
4988const codecJson = Schema.toCodecJson(PointSchema)
4989
4990console.log(String(Schema.encodeUnknownExit(codecJson)(new Point(1, 2))))
4991// Failure(Cause([Fail(SchemaError(Expected JSON value))]))
4992
4993console.log(String(Schema.decodeUnknownExit(codecJson)({ x: 1, y: 2 })))
4994// Failure(Cause([Fail(SchemaError(Expected <Declaration>))]))
4995```
4996
4997The same issue shows up when generating a JSON Schema document. Since the opaque declaration has no structural JSON representation, the generator emits an unconstrained JSON Schema:
4998
4999```ts
5000console.log(Schema.toJsonSchemaDocument(PointSchema))
5001// { dialect: 'draft-2020-12', schema: {}, definitions: {} }
5002```
5003
5004#### Configuring the Codec
5005
5006You configure the canonical JSON codec by adding a `toCodecJson` annotation to your schema.
5007
5008Then you call `Schema.toCodecJson(schema)` to produce a codec schema that can encode and decode values to and from JSON-compatible data.
5009
5010**Example** (Encoding a class as a JSON tuple)
5011
5012```ts
5013import { Schema, SchemaTransformation } from "effect"
5014
5015class Point {
5016 constructor(public readonly x: number, public readonly y: number) {}
5017
5018 distance(other: Point): number {
5019 const dx = this.x - other.x
5020 const dy = this.y - other.y
5021 return Math.sqrt(dx * dx + dy * dy)
5022 }
5023}
5024
5025const PointSchema = Schema.instanceOf(Point, {
5026 toCodecJson: () =>
5027 Schema.link<Point>()(
5028 // Pick a JSON representation for Point.
5029 // Here we use a fixed-length tuple: [x, y].
5030 Schema.Tuple([Schema.Finite, Schema.Finite]),
5031 SchemaTransformation.transform({
5032 // Decode: convert the JSON representation into a Point instance.
5033 decode: (args) => new Point(...args),
5034
5035 // Encode: convert a Point instance into the JSON representation.
5036 encode: (instance) => [instance.x, instance.y] as const
5037 })
5038 )
5039})
5040
5041// Convert the schema into a JSON codec schema.
5042const codecJson = Schema.toCodecJson(PointSchema)
5043
5044// Encoding produces JSON-safe data, so it can be stringified.
5045console.log(JSON.stringify(Schema.encodeUnknownSync(codecJson)(new Point(1, 2))))
5046// "[1,2]"
5047
5048// Decoding rebuilds the Point instance from parsed JSON.
5049console.log(Schema.decodeUnknownSync(codecJson)(JSON.parse("[1,2]")))
5050// Point { x: 1, y: 2 }
5051
5052// JSON Schema generation now has a real representation to work with.
5053console.dir(Schema.toJsonSchemaDocument(PointSchema), { depth: null })
5054/*
5055{
5056 dialect: 'draft-2020-12',
5057 schema: {
5058 type: 'array',
5059 prefixItems: [ { type: 'number' }, { type: 'number' } ],
5060 maxItems: 2,
5061 minItems: 2
5062 },
5063 definitions: {}
5064}
5065*/
5066```
5067
5068When you use `toCodecJson`, you describe the JSON shape once (in the schema), and Effect can reuse that description in two places:
5069
5070- `Schema.toCodecJson(...)` uses it to encode and decode JSON data at runtime.
5071- `Schema.toJsonSchemaDocument(...)` uses it to produce a JSON Schema document for the same JSON shape.
5072
5073Because both outputs come from the same annotation, they describe the same format (in this example, a two-item array `[x, y]`). If you change the JSON representation in `toCodecJson`, both the codec and the generated JSON Schema will change with it.
5074
5075You can use the JSON Schema to validate or describe the JSON data (for example in OpenAPI), and use the codec schema to encode and decode values in that same format.
5076
5077#### How `toCodecJson` Works
5078
5079When you call `Schema.toCodecJson(schema)`, the library:
5080
50811. **Walks the AST**: it traverses the schema's abstract syntax tree (AST) recursively. For details, see the `SchemaAST` module.
50822. **Finds annotations**: it looks for `toCodecJson` annotations on nodes.
50833. **Applies transformations**: it replaces types that are not JSON-friendly with types that are.
50844. **Composes recursively**: it builds codecs for nested schemas by combining the codecs of their parts.
5085
5086When this traversal adds a transformation because a node has no direct JSON representation, its checks and annotations remain on the source node; they are not copied to the artificial JSON target. The codec still applies source checks after transforming values.
5087
5088#### Custom Encodings
5089
5090`Schema.toCodecJson` respects **explicit encodings** you add to a schema. If you choose a custom representation, that choice takes priority over the default.
5091
5092**Example** (Custom encoding takes priority over default Date handling)
5093
5094```ts
5095import { Schema, SchemaTransformation } from "effect"
5096
5097// Custom Date encoding (Date -> number)
5098const DateFromEpochMillis = Schema.Date.pipe(
5099 Schema.encodeTo(
5100 Schema.Number,
5101 SchemaTransformation.transform({
5102 decode: (epochMillis) => new Date(epochMillis),
5103 encode: (date) => date.getTime()
5104 })
5105 )
5106)
5107
5108const schema = Schema.Struct({
5109 date1: DateFromEpochMillis,
5110 date2: Schema.Date
5111})
5112
5113const toCodecJson = Schema.toCodecJson(schema)
5114
5115const data = { date1: new Date("2021-01-01"), date2: new Date("2021-01-01") }
5116
5117const serialized = Schema.encodeUnknownSync(toCodecJson)(data)
5118console.log(serialized)
5119// { date1: 1609459200000, date2: "2021-01-01T00:00:00.000Z" }
5120// date1 uses your custom number format, date2 uses the default ISO string format
5121```
5122
5123### StringTree Canonical Codec
5124
5125The `StringTree` codec converts all values to strings, keeping the structure but not the original types.
5126
5127```ts
5128type StringTree = string | undefined | { readonly [key: string]: StringTree } | ReadonlyArray<StringTree>
5129```
5130
5131A StringTree codec turns any value into a structure made only of:
5132
5133- strings
5134- `undefined`
5135- plain objects containing other `StringTree` values
5136- arrays of `StringTree` values
5137
5138#### toCodecJson vs toCodecStringTree
5139
5140**Example** (Comparing JSON and StringTree codecs)
5141
5142```ts
5143import { Schema, SchemaTransformation } from "effect"
5144
5145class Point {
5146 constructor(public readonly x: number, public readonly y: number) {}
5147
5148 distance(other: Point): number {
5149 const dx = this.x - other.x
5150 const dy = this.y - other.y
5151 return Math.sqrt(dx * dx + dy * dy)
5152 }
5153}
5154
5155const PointSchema = Schema.instanceOf(Point, {
5156 toCodecJson: () =>
5157 Schema.link<Point>()(
5158 Schema.Tuple([Schema.Finite, Schema.Finite]),
5159 SchemaTransformation.transform({
5160 decode: (args) => new Point(...args),
5161 encode: (instance) => [instance.x, instance.y] as const
5162 })
5163 )
5164})
5165
5166const point = new Point(1, 2)
5167
5168const toCodecJson = Schema.toCodecJson(PointSchema)
5169
5170const json = Schema.encodeUnknownSync(toCodecJson)(point)
5171
5172// keeps numbers as numbers
5173console.log(json)
5174// [1, 2]
5175
5176const toCodecStringTree = Schema.toCodecStringTree(PointSchema)
5177
5178const stringTree = Schema.encodeUnknownSync(toCodecStringTree)(point)
5179
5180// every leaf value becomes a string
5181console.log(stringTree)
5182// [ '1', '2' ]
5183```
5184
5185### ISO Canonical Codec
5186
5187The ISO canonical codec (`toCodecIso`) converts schemas to their `Iso` representation. This is useful when you want to build isomorphic transformations or optics.
5188
5189**Example** (Using the ISO canonical codec with a Class)
5190
5191```ts
5192import { Schema } from "effect"
5193
5194// Define a class schema
5195class Person extends Schema.Class<Person>("Person")({
5196 name: Schema.String,
5197 age: Schema.Number
5198}) {}
5199
5200const codecIso = Schema.toCodecIso(Person)
5201
5202// The Iso type represents the "focus" of the schema.
5203// For Class schemas, the Iso type is the struct representation
5204// of the class fields: { readonly name: string; readonly age: number }
5205// This allows you to convert between the class instance and a plain object
5206// with the same shape, which is useful for optics and transformations.
5207
5208const person = new Person({ name: "John", age: 30 })
5209
5210const serialized = Schema.encodeUnknownSync(codecIso)(person)
5211console.log(serialized)
5212// { name: 'John', age: 30 }
5213
5214const deserialized = Schema.decodeUnknownSync(codecIso)(serialized)
5215console.log(deserialized)
5216// Person { name: 'John', age: 30 }
5217```
5218
5219ISO serializers are mainly used internally for building optics and reusable transformations.
5220
5221## XML Encoder
5222
5223`Schema.toEncoderXml` lets you serialize values to XML.
5224It uses the `toCodecStringTree` serializer internally.
5225
5226**Example**
5227
5228```ts
5229import { Effect, Option, Schema } from "effect"
5230
5231const schema = Schema.Struct({
5232 a: Schema.String,
5233 b: Schema.Array(Schema.NullOr(Schema.String)),
5234 c: Schema.Struct({
5235 d: Schema.Option(Schema.String),
5236 e: Schema.Date
5237 }),
5238 f: Schema.optional(Schema.String)
5239})
5240
5241// const encoder: (t: {...}) => Effect<string, Schema.SchemaError, never>
5242const xmlEncoder = Schema.toEncoderXml(schema)
5243
5244console.log(
5245 Effect.runSync(
5246 xmlEncoder({
5247 a: "",
5248 b: ["bar", "baz", null],
5249 c: { d: Option.some("qux"), e: new Date("2021-01-01") },
5250 f: undefined
5251 })
5252 )
5253)
5254/*
5255<root>
5256 <a></a>
5257 <b>
5258 <item>bar</item>
5259 <item>baz</item>
5260 <item/>
5261 </b>
5262 <c>
5263 <d>
5264 <_tag>Some</_tag>
5265 <value>qux</value>
5266 </d>
5267 <e>2021-01-01T00:00:00.000Z</e>
5268 </c>
5269 <f/>
5270</root>
5271*/
5272```
5273
5274**Note**. Schemas representing custom types are encoded as `undefined`:
5275
5276# Schema Generation and Tooling
5277
5278Schema can derive JSON Schemas, test data generators (Arbitraries), equivalence checks, optics, and more from a single schema definition.
5279
5280### Generating a JSON Schema from a Schema
5281
5282#### Basic Conversion
5283
5284By default, a schema produces a draft-2020-12 JSON Schema.
5285
5286The generated document is intended for preliminary validation. JSON Schema and
5287Effect checks do not always have identical semantics, so the Effect decoder
5288remains the final authority. Passing JSON Schema validation does not guarantee
5289that decoding will succeed.
5290
5291Properties not modeled by an object schema use `onExcessProperty: "ignore"` by
5292default, matching the decoder default. This emits `additionalProperties: true`.
5293Pass `{ onExcessProperty: "error" }` to the generator and decoder to reject
5294unmatched properties whenever the key space is representable. An
5295index-signature key check that cannot be translated to an exact selector uses
5296a conservative fallback so that JSON Schema does not reject inputs Effect may accept.
5297The generator does not merge conjunctive key patterns into a new regular
5298expression. With `onExcessProperty: "ignore"`, it leaves that index signature
5299open. With `onExcessProperty: "error"`, it uses the generated key schemas under
5300`propertyNames`. Properties not already selected by `properties` or
5301`patternProperties` may satisfy any index-signature value schema; the Effect
5302decoder enforces the exact association between keys and values.
5303
5304Known differences include Unicode code-point versus UTF-16 string length,
5305JavaScript RegExp flags, and property checks applied before versus after decoding.
5306When a `oneOf` branch has a known approximation, the compiler emits `anyOf` so
5307that newly overlapping branches cannot reject valid values. It retains `oneOf`
5308when all branches are exact. Custom `toJsonSchema` callbacks declare approximate
5309results with `[schema, true]`; see [Check exporters and approximation](#check-exporters-and-approximation).
5310
5311The result is a data structure including:
5312
5313- the source of the JSON Schema (e.g. `draft-2020-12`, `draft-07`, etc...)
5314- the JSON Schema itself
5315- any definitions referenced by `$ref` (if any)
5316
5317**Example** (Tuple to draft-2020-12 JSON Schema)
5318
5319```ts
5320import { Schema } from "effect"
5321
5322// Define a tuple: [string, number]
5323const schema = Schema.Tuple([Schema.String, Schema.Finite])
5324
5325// Generate a draft-2020-12 JSON Schema
5326const document = Schema.toJsonSchemaDocument(schema)
5327
5328console.log(JSON.stringify(document, null, 2))
5329/*
5330Output:
5331{
5332 "source": "draft-2020-12",
5333 "schema": {
5334 "type": "array",
5335 "prefixItems": [
5336 {
5337 "type": "string"
5338 },
5339 {
5340 "type": "number"
5341 }
5342 ],
5343 "maxItems": 2,
5344 "minItems": 2
5345 },
5346 "definitions": {}
5347}
5348*/
5349```
5350
5351To generate a draft-07 JSON Schema, use `JsonSchema.toDocumentDraft07` to convert the draft-2020-12 JSON Schema.
5352
5353The dialect converters preserve unknown and custom keywords as opaque values. When a known keyword cannot be
5354represented by the target dialect, or an opaque keyword would become active and change meaning in that dialect,
5355conversion throws instead of silently changing its meaning. For example, Draft-07 cannot represent dynamic
5356references, unevaluated constraints, or non-default `minContains` / `maxContains` cardinality.
5357
5358**Example** (Tuple to draft-7 JSON Schema)
5359
5360```ts
5361import { JsonSchema, Schema } from "effect"
5362
5363const schema = Schema.Tuple([Schema.String, Schema.Finite])
5364
5365const doc2020_12 = Schema.toJsonSchemaDocument(schema)
5366const doc07 = JsonSchema.toDocumentDraft07(doc2020_12)
5367
5368console.log(JSON.stringify(doc07, null, 2))
5369/*
5370Output:
5371{
5372 "source": "draft-07",
5373 "schema": {
5374 "type": "array",
5375 "maxItems": 2,
5376 "minItems": 2,
5377 "items": [
5378 {
5379 "type": "string"
5380 },
5381 {
5382 "type": "number"
5383 }
5384 ]
5385 },
5386 "definitions": {}
5387}
5388*/
5389```
5390
5391#### Attaching Standard Metadata
5392
5393Use `.annotate(...)` to attach standard JSON Schema annotations:
5394
5395- `title`
5396- `description`
5397- `default`
5398- `examples`
5399- `readOnly`
5400- `writeOnly`
5401
5402`Schema.toJsonSchemaDocument` compiles the canonical JSON target. Checks and annotations left on the source side of an artificial transformation therefore do not appear in the generated document. Put metadata on an explicit JSON target when it must appear there.
5403
5404**Example** (Adding basic annotations)
5405
5406```ts
5407import { Schema } from "effect"
5408
5409const schema = Schema.NonEmptyString.annotate({
5410 title: "Username",
5411 description: "A non-empty user name string",
5412 default: "anonymous",
5413 examples: ["alice", "bob"]
5414})
5415
5416const document = Schema.toJsonSchemaDocument(schema)
5417
5418console.log(JSON.stringify(document, null, 2))
5419/*
5420{
5421 "source": "draft-2020-12",
5422 "schema": {
5423 "type": "string",
5424 "allOf": [
5425 {
5426 "minLength": 1,
5427 "title": "Username",
5428 "description": "A non-empty user name string",
5429 "default": "anonymous",
5430 "examples": [
5431 "alice",
5432 "bob"
5433 ]
5434 }
5435 ]
5436 },
5437 "definitions": {}
5438}
5439*/
5440```
5441
5442#### Annotating the Encoded Side of a Transformation
5443
5444When a schema includes a transformation (e.g. `Schema.Trim`), the generated JSON Schema corresponds to the encoded side. Calling `.annotate(...)` on a transformation annotates the decoded side, so the annotations won't appear in the JSON Schema output.
5445
5446To annotate the encoded side, use `Schema.annotateEncoded`.
5447
5448**Example** (Annotating the encoded side of `Trim`)
5449
5450```ts
5451import { Schema } from "effect"
5452
5453const schema = Schema.Trim.pipe(
5454 Schema.annotateEncoded({
5455 description: "my description",
5456 title: "my title"
5457 })
5458)
5459
5460console.log(JSON.stringify(Schema.toJsonSchemaDocument(schema), null, 2))
5461/*
5462{
5463 "dialect": "draft-2020-12",
5464 "schema": {
5465 "type": "string",
5466 "title": "my title",
5467 "description": "my description"
5468 },
5469 "definitions": {}
5470}
5471*/
5472```
5473
5474Alternatively, build a custom transformation using `Schema.decodeTo`:
5475
5476```ts
5477import { Schema, SchemaTransformation } from "effect"
5478
5479const schema = Schema.String.annotate({
5480 description: "my description",
5481 title: "my title"
5482}).pipe(Schema.decodeTo(Schema.Trimmed, SchemaTransformation.trim()))
5483
5484console.log(JSON.stringify(Schema.toJsonSchemaDocument(schema), null, 2))
5485/*
5486{
5487 "dialect": "draft-2020-12",
5488 "schema": {
5489 "type": "string",
5490 "title": "my title",
5491 "description": "my description"
5492 },
5493 "definitions": {}
5494}
5495*/
5496```
5497
5498#### Optional fields / elements
5499
5500Optional fields are converted to optional fields or elements in the JSON Schema.
5501
5502**Example**
5503
5504```ts
5505import { Schema } from "effect"
5506
5507const schema = Schema.Struct({
5508 a: Schema.optionalKey(Schema.String)
5509})
5510
5511const document = Schema.toJsonSchemaDocument(schema)
5512
5513console.log(JSON.stringify(document, null, 2))
5514/*
5515{
5516 "source": "draft-2020-12",
5517 "schema": {
5518 "type": "object",
5519 "properties": {
5520 "a": {
5521 "type": "string"
5522 }
5523 },
5524 "additionalProperties": true
5525 },
5526 "definitions": {}
5527}
5528*/
5529```
5530
5531Fields including `undefined` (such as those defined unsing `Schema.optional` or `Schema.UndefinedOr`) are converted to optional fields or elements in the JSON Schema with a union with the `null` type.
5532
5533**Example**
5534
5535```ts
5536import { Schema } from "effect"
5537
5538const schema = Schema.Struct({
5539 a: Schema.optional(Schema.String)
5540})
5541
5542const document = Schema.toJsonSchemaDocument(schema)
5543
5544console.log(JSON.stringify(document, null, 2))
5545/*
5546{
5547 "source": "draft-2020-12",
5548 "schema": {
5549 "type": "object",
5550 "properties": {
5551 "a": {
5552 "anyOf": [
5553 {
5554 "type": "string"
5555 },
5556 {
5557 "type": "null"
5558 }
5559 ]
5560 }
5561 },
5562 "additionalProperties": true
5563 },
5564 "definitions": {}
5565}
5566*/
5567```
5568
5569#### Defining a JSON-safe representation for custom types
5570
5571This example shows how `Schema.toCodecJson` and `Schema.toJsonSchemaDocument` can describe the same JSON shape for a custom type.
5572
5573`Headers` is not JSON-friendly by default. `JSON.stringify(new Headers({ a: "b" }))` produces `{}` because the header data is not stored in enumerable properties. By adding a `toCodecJson` annotation, you define a JSON-safe representation and use it for both serialization and JSON Schema generation.
5574
5575**Example** (Align a JSON serializer and JSON Schema for `Headers`)
5576
5577```ts
5578import { Schema, SchemaGetter } from "effect"
5579
5580const data = new Headers({ a: "b" })
5581
5582// `Headers` does not serialize to JSON in a useful way by default.
5583console.log(JSON.stringify(data))
5584// {}
5585
5586// Define a schema with a `toCodecJson` annotation.
5587// The JSON form will be: [ [name, value], ... ].
5588const MyHeaders = Schema.instanceOf(Headers, {
5589 toCodecJson: () =>
5590 Schema.link<Headers>()(
5591 // JSON-safe representation: array of [key, value] pairs
5592 Schema.Array(Schema.Tuple([Schema.String, Schema.String])),
5593 {
5594 decode: SchemaGetter.transform((headers) => new Headers(headers.map(([key, value]) => [key, value]))),
5595 encode: SchemaGetter.transform((headers) => [...headers.entries()])
5596 }
5597 )
5598})
5599
5600const schema = Schema.Struct({
5601 headers: MyHeaders
5602})
5603
5604// Build a serializer that produces JSON-safe values using the `toCodecJson` annotation.
5605const serializer = Schema.toCodecJson(schema)
5606
5607const json = Schema.encodeUnknownSync(serializer)({
5608 headers: data
5609})
5610
5611// The JSON-encoded value:
5612console.log(json)
5613// { headers: [ [ 'a', 'b' ] ] }
5614
5615// Generate a JSON Schema that matches the JSON-safe shape produced by the serializer.
5616const document = Schema.toJsonSchemaDocument(schema)
5617
5618console.log(JSON.stringify(document.schema, null, 2))
5619/*
5620{
5621 "type": "object",
5622 "properties": {
5623 "headers": {
5624 "type": "array",
5625 "items": {
5626 "type": "array",
5627 "prefixItems": [
5628 {
5629 "type": "string"
5630 },
5631 {
5632 "type": "string"
5633 }
5634 ],
5635 "maxItems": 2,
5636 "minItems": 2
5637 }
5638 }
5639 },
5640 "required": [
5641 "headers"
5642 ],
5643 "additionalProperties": true
5644}
5645*/
5646
5647// Example (Decode a JSON-safe value using the same serializer)
5648// JSON Schema is the preliminary check; the serializer remains the final validator.
5649console.log(String(Schema.decodeUnknownExit(serializer)(json)))
5650// Success({"headers":Headers([["a","b"]])})
5651```
5652
5653#### Validation Constraints
5654
5655**Example**
5656
5657```ts
5658import { Schema } from "effect"
5659
5660const schema = Schema.String.check(Schema.isMinLength(1))
5661
5662const document = Schema.toJsonSchemaDocument(schema)
5663
5664console.log(JSON.stringify(document, null, 2))
5665/*
5666{
5667 "source": "draft-2020-12",
5668 "schema": {
5669 "type": "string",
5670 "allOf": [
5671 {
5672 "minLength": 1
5673 }
5674 ]
5675 },
5676 "definitions": {}
5677}
5678*/
5679```
5680
5681**Example** (Multiple filters)
5682
5683```ts
5684import { Schema } from "effect"
5685
5686const schema = Schema.String.check(
5687 Schema.isMinLength(1, { description: "description1" }),
5688 Schema.isMaxLength(2, { description: "description2" })
5689)
5690
5691const document = Schema.toJsonSchemaDocument(schema)
5692
5693console.log(JSON.stringify(document, null, 2))
5694/*
5695{
5696 "source": "draft-2020-12",
5697 "schema": {
5698 "type": "string",
5699 "allOf": [
5700 {
5701 "minLength": 1,
5702 "description": "description1"
5703 },
5704 {
5705 "maxLength": 2,
5706 "description": "description2"
5707 }
5708 ]
5709 },
5710 "definitions": {}
5711}
5712*/
5713```
5714
5715#### The fromJsonString combinator
5716
5717With `fromJsonString`, the generated schema uses `contentSchema` to embed the JSON Schema of the decoded value.
5718
5719**Example** (Embedding `contentSchema` for JSON string content)
5720
5721```ts
5722import { Schema } from "effect"
5723
5724// Original value is an object with a string field 'a'
5725const original = Schema.Struct({ a: Schema.String })
5726
5727// fromJsonString: the outer value is a string,
5728// but its content must be valid JSON matching 'original'
5729const schema = Schema.fromJsonString(original)
5730
5731const document = Schema.toJsonSchemaDocument(schema)
5732
5733console.log(JSON.stringify(document, null, 2))
5734/*
5735{
5736 "source": "draft-2020-12",
5737 "schema": {
5738 "type": "string",
5739 "contentMediaType": "application/json",
5740 "contentSchema": {
5741 "type": "object",
5742 "properties": {
5743 "a": {
5744 "type": "string"
5745 }
5746 },
5747 "required": [
5748 "a"
5749 ],
5750 "additionalProperties": true
5751 }
5752 },
5753 "definitions": {}
5754}
5755*/
5756```
5757
5758### Generating an Equivalence from a Schema
5759
5760An equivalence function checks whether two values are structurally equal according to the schema's definition. Schema derives this automatically, so you do not need to write manual comparison logic.
5761
5762**Example** (Deriving equivalence for a basic schema)
5763
5764```ts
5765import { Schema } from "effect"
5766
5767const schema = Schema.Struct({
5768 a: Schema.String,
5769 b: Schema.Number
5770})
5771
5772const equivalence = Schema.toEquivalence(schema)
5773```
5774
5775#### Declarations
5776
5777**Example** (Providing a custom equivalence for a class)
5778
5779```ts
5780import { Schema } from "effect"
5781
5782class MyClass {
5783 constructor(readonly a: string) {}
5784}
5785
5786const schema = Schema.instanceOf(MyClass, {
5787 toEquivalence: () => (x, y) => x.a === y.a
5788})
5789
5790const equivalence = Schema.toEquivalence(schema)
5791```
5792
5793#### Overrides
5794
5795You can override the derived equivalence for a schema using `overrideToEquivalence`. This is useful when the default derivation does not fit your requirements.
5796
5797**Example** (Overriding equivalence for a struct)
5798
5799```ts
5800import { Equivalence, Schema } from "effect"
5801
5802const schema = Schema.Struct({
5803 a: Schema.String,
5804 b: Schema.Number
5805}).pipe(Schema.overrideToEquivalence(() => Equivalence.make((x, y) => x.a === y.a)))
5806
5807const equivalence = Schema.toEquivalence(schema)
5808```
5809
5810### Generating an Optic from a Schema
5811
5812Optics provide a composable way to read and update deeply nested values without mutating the original object. Schema can derive optics automatically from your schema definition.
5813
5814#### Problem
5815
5816The `Optic` module only works with plain JavaScript objects and collections (structs, records, tuples, and arrays).
5817This can feel restrictive when working with custom types.
5818
5819To work around this, you can define an `Iso` between your custom type and a plain JavaScript object.
5820
5821**Example** (Defining an `Iso` manually between a custom type and a plain JavaScript object)
5822
5823```ts
5824import { Optic, Schema } from "effect"
5825
5826// Define custom schema-based classes
5827class A extends Schema.Class<A>("A")({ s: Schema.String }) {}
5828class B extends Schema.Class<B>("B")({ a: A }) {}
5829
5830// Create an Iso that converts between B and a plain object
5831const iso = Optic.makeIso<B, { readonly a: { readonly s: string } }>(
5832 (s) => ({ a: { s: s.a.s } }), // forward transformation
5833 (a) => new B({ a: new A({ s: a.a.s }) }) // backward transformation
5834)
5835
5836// Build an optic that drills down to the "s" field inside "a"
5837const _s = iso.key("a").key("s")
5838
5839console.log(_s.replace("b", new B({ a: new A({ s: "a" }) })))
5840// B { a: A { s: 'b' } }
5841```
5842
5843#### Solution
5844
5845Manually creating `Iso` instances is repetitive and error-prone.
5846To simplify this, the library provides a helper function that generates an `Iso` directly from a schema.
5847
5848This allows you to keep working with plain JavaScript objects and collections while still benefiting from schema definitions.
5849
5850**Example** (Generating an `Iso` automatically from a schema)
5851
5852```ts
5853import { Schema } from "effect"
5854
5855class A extends Schema.Class<A>("A")({ s: Schema.String }) {}
5856class B extends Schema.Class<B>("B")({ a: A }) {}
5857
5858// Automatically generate an Iso from the schema of B
5859// const iso: Iso<B, { readonly a: { readonly s: string } }>
5860const iso = Schema.toIso(B)
5861
5862const _s = iso.key("a").key("s")
5863
5864console.log(_s.replace("b", new B({ a: new A({ s: "a" }) })))
5865// B { a: A { s: 'b' } }
5866```
5867
5868Reading through the generated `Iso` encodes the schema value, while replacing through it decodes the new focus. `Schema.toIso` consumes `Schema.toCodecIso` synchronously, so failing, asynchronous, or service-dependent transformations can throw. Schema failures use `"Schema validation failed"` with a `SchemaIssue.Issue` in `cause`; format it with `SchemaIssue.makeFormatterDefault()`, or use an effectful parser with `Schema.toCodecIso` for asynchronous execution or explicit failure handling.
5869
5870### Using the Differ Module for Type-Safe JSON Patches
5871
5872The `Differ` module lets you compute and apply JSON Patch (RFC 6902) changes for any value described by a `Schema`. You give it a schema once, then use the returned differ to produce a patch from an old value to a new value, and to apply that patch.
5873
5874**Example** (Compare two values and apply the patch)
5875
5876```ts
5877import { Schema } from "effect"
5878
5879// Describe the shape of your data
5880const schema = Schema.Struct({
5881 id: Schema.Number,
5882 name: Schema.String,
5883 price: Schema.Number
5884})
5885
5886// Build a differ tied to the schema
5887const differ = Schema.toDifferJsonPatch(schema)
5888
5889// Prepare two values to compare
5890const oldValue = { id: 1, name: "a", price: 1 }
5891const newValue = { id: 1, name: "b", price: 2 }
5892
5893// Compute a JSON Patch document (an array of operations)
5894const jsonPatch = differ.diff(oldValue, newValue)
5895console.log(jsonPatch)
5896/*
5897[
5898 { op: 'replace', path: '/name', value: 'b' },
5899 { op: 'replace', path: '/price', value: 2 }
5900]
5901*/
5902
5903// Apply the patch to the old value to get the new value
5904const patched = differ.patch(oldValue, jsonPatch)
5905console.log(patched)
5906// { id: 1, name: 'b', price: 2 }
5907```
5908
5909#### Works with custom types too
5910
5911**Example** (Compare two custom types)
5912
5913```ts
5914import { Schema } from "effect"
5915
5916class A extends Schema.Class<A>("A")({ n: Schema.Number }) {}
5917class B extends Schema.Class<B>("B")({ a: A }) {}
5918
5919const differ = Schema.toDifferJsonPatch(B)
5920
5921const oldValue = new B({ a: new A({ n: 0 }) })
5922const newValue = new B({ a: new A({ n: 1 }) })
5923
5924const patch = differ.diff(oldValue, newValue)
5925console.log(patch)
5926// [ { op: 'replace', path: '/a/n', value: 1 } ]
5927
5928console.log(differ.patch(oldValue, patch))
5929// B { a: A { n: 1 } }
5930```
5931
5932#### How it works
5933
5934The idea is simple: if you have a `Schema` for a type `T`, you can serialize any `T` to JSON and back. That lets us compute and apply JSON Patch on the JSON view, while keeping the public API typed as `T`.
5935
5936- `diff(oldValue, newValue)`
5937
5938 1. Encode `oldValue: T` and `newValue: T` to JSON with the schema serializer.
5939 2. Compute a JSON Patch document between the two JSON values.
5940 3. Return that patch (an array of `"add" | "remove" | "replace"` operations).
5941
5942- `patch(oldValue, patch)`
5943 1. Encode `oldValue: T` to JSON.
5944 2. Apply the JSON Patch to the JSON value.
5945 3. Decode the patched JSON back to `T` using the schema.
5946
5947This approach keeps patches independent from TypeScript types and uses the schema as the guardrail when turning JSON back into `T`.
5948
5949`Schema.toDifferJsonPatch` consumes `Schema.toCodecJson` synchronously, so failing, asynchronous, or service-dependent transformations can throw. Schema failures use `"Schema validation failed"` with a `SchemaIssue.Issue` in `cause`, which `SchemaIssue.makeFormatterDefault()` can format; invalid patch operations instead produce `JsonPatch` errors.
5950
5951# Schema Representation
5952
5953The `SchemaRepresentation` module exposes the structural form used to inspect, persist, compile, and rebuild schemas.
5954
5955A representation is always a projection of one side of a schema. By default, `Schema.toRepresentation` and
5956`SchemaRepresentation.toRepresentation` project the encoded side. Apply `Schema.toType` or `SchemaAST.toType` first when
5957you need the decoded type side instead.
5958
5959Use it when you need to:
5960
5961- inspect the structural form of a schema
5962- store schemas on disk or send them over the network
5963- rebuild runtime schemas with an explicit set of revivers
5964- compile live representations to JSON Schema Draft 2020-12
5965- generate TypeScript code from live representations
5966
5967At a high level:
5968
5969- `Schema.toRepresentation(schema)` converts a schema to a `Document`
5970- `SchemaRepresentation.toRepresentation(ast)` and `toRepresentations(asts)` convert schema ASTs to a `Document` or
5971 `MultiDocument`
5972- `toJson` / `fromJson` cross the persistence boundary
5973- `fromRepresentation` / `fromRepresentations` rebuild runtime schemas using explicit revivers
5974- `toJsonSchemaDocument` compiles a live `Document` to JSON Schema Draft 2020-12
5975- `toCodeDocument` compiles a live `MultiDocument` to runtime and TypeScript source fragments
5976
5977```mermaid
5978flowchart TD
5979 S[Schema] -->|Schema.toRepresentation|LD["live Document"]
5980 AST[SchemaAST] -->|SchemaRepresentation.toRepresentation|LD
5981 LD -->|toJson|JSON["JSON value"]
5982 JSON -->|fromJson|PD["persisted Document"]
5983 PD -->|"fromRepresentation + revivers"|S
5984 LD -->|toJsonSchemaDocument|JD["JsonSchema.Document (draft-2020-12)"]
5985 JD -->|fromJsonSchemaDocument|S
5986 LD -->|toMultiDocument|LMD["live MultiDocument"]
5987 LMD -->|toCodeDocument|CodeDocument
5988 LMD -->|toJsonSchemaMultiDocument|JMD[JsonSchema.MultiDocument]
5989 LMD -->|toJsonMultiDocument|JSON
5990```
5991
5992## The data model
5993
5994### `Representation`
5995
5996A `Representation` is a tagged object tree (`_tag` fields like `"String"`, `"Objects"`, `"Union"`, ...). It describes one
5997structural side of a schema. Named or recursive nodes use `Reference` values instead of duplicating their definitions.
5998
5999### `Document`
6000
6001A `Document` has:
6002
6003- `representation`: the root `Representation`
6004- `references`: a map of named definitions used by the root representation
6005
6006References let the representation share definitions and support recursion.
6007
6008### `MultiDocument`
6009
6010A `MultiDocument` stores multiple root representations that share the same `references` table.
6011
6012This is useful if you want to serialize a set of schemas together, or if you want to generate code for multiple schemas while emitting shared definitions only once.
6013
6014## Projection and persistence boundaries
6015
6016### Representations use the encoded side
6017
6018`toRepresentation` follows a schema's encoding chain and represents its last encoded side. It does not serialize the
6019transformation functions.
6020
6021```ts
6022import { Schema } from "effect"
6023
6024const encoded = Schema.toRepresentation(Schema.NumberFromString)
6025console.log(encoded.representation._tag)
6026// "String"
6027
6028const decoded = Schema.toRepresentation(Schema.toType(Schema.NumberFromString))
6029console.log(decoded.representation._tag)
6030// "Number"
6031```
6032
6033Consequently, rebuilding `encoded` produces a schema for the string representation; it does not recreate the original
6034string-to-number transformation.
6035
6036Representations describe runtime schema structure, so they do not preserve
6037TypeScript-only distinctions. `Schema.brand` is absent because it does not
6038change the AST. A check introduced by `Schema.refine` can remain part of the
6039runtime representation, but its narrowed TypeScript type cannot be recovered.
6040Reapply these type-level operations after rebuilding a representation or
6041generating schema code when needed.
6042
6043### Live and persisted documents
6044
6045A live `Document` can contain functions in its ordinary annotations. These callbacks allow compilers to handle custom
6046behavior:
6047
6048- a check can provide `toJsonSchema`
6049- a declaration or check can provide `toCode`
6050
6051Functions cannot cross the JSON persistence boundary. `toJson` removes them and keeps only JSON-valued ordinary
6052annotations. Nested JSON arrays and objects are preserved; a complete annotation value is omitted when it contains a
6053function, `undefined`, `bigint`, a symbol, a cycle, or another non-JSON value.
6054
6055Structural values such as bigint literals and registered unique symbols have dedicated canonical encodings. That does not
6056make bigint or symbol values valid generic annotations.
6057
6058### Persistence identities
6059
6060Opaque declarations and checks need a stable identity before they can be persisted:
6061
6062```ts
6063interface RepresentationAnnotation {
6064 readonly id: string
6065 readonly payload: Schema.Json
6066}
6067
6068interface CheckRepresentationAnnotation<S> extends RepresentationAnnotation {
6069 readonly schemas?: ReadonlyArray<S>
6070}
6071```
6072
6073`id` selects a reviver, `payload` contains its JSON configuration, and a check can use `schemas` for schema dependencies.
6074This replaces the previous closed set of check metadata. Custom declarations and checks are therefore persistable when
6075they provide a representation identity and the consumer provides a matching reviver.
6076
6077An unannotated custom declaration or leaf filter can still exist in a live representation, but `toJson` rejects it because
6078there is no portable way to reconstruct its user code.
6079
6080## Creating representations
6081
6082Use `Schema.toRepresentation` when starting from a schema:
6083
6084```ts
6085import { Schema } from "effect"
6086
6087const document = Schema.toRepresentation(
6088 Schema.Struct({ name: Schema.NonEmptyString })
6089)
6090```
6091
6092Use the lower-level functions when working directly with ASTs or several roots:
6093
6094```ts
6095import { Schema, SchemaRepresentation } from "effect"
6096
6097const document = SchemaRepresentation.toRepresentation(Schema.String.ast)
6098
6099const multiDocument = SchemaRepresentation.toRepresentations([
6100 Schema.String.ast,
6101 Schema.Number.ast
6102])
6103```
6104
6105By default, only candidates with a resolved identifier are placed in `references`. Anonymous non-recursive nodes remain
6106inline even when the same AST occurs more than once. Recursive schemas always require a reference; when no identifier is
6107available, the converter assigns a synthetic name such as `Objects_` or `Suspend_`.
6108
6109The default policy uses an explicit `identifier` as the reference name. Reusing the same schema shares its reference.
6110AST copies that change only their own `context` or `encoding` share a decoded body. Type and encoded projections preserve
6111this sharing while keeping each occurrence's context and following its actual encoding chain. Changing checks, value
6112annotations, or children creates a distinct body; child contexts are part of the parent's structure. Reference owners omit
6113their own encoding, so reference policies inspect the represented body. Independently constructed ASTs are not
6114canonicalized merely because they are structurally equal. When distinct schemas request the same name, the first schema
6115keeps it and later schemas receive numeric suffixes in encounter order,
6116such as `Value_1` and `Value_2`. Internal `~identifier` annotations are fallback allocation hints; their generated names
6117use the `Encoded` suffix and follow the same collision rules.
6118
6119`toMultiDocument(document)` wraps a single document when a compiler requires multiple roots.
6120
6121### Reference policies
6122
6123`Schema.toRepresentation`, `SchemaRepresentation.toRepresentation`, and `SchemaRepresentation.toRepresentations` accept
6124an optional `referencePolicy`. The policy runs once for every candidate, after occurrences across all roots have been
6125counted:
6126
6127```ts
6128interface ReferencePolicyInput {
6129 readonly ast: SchemaAST.AST
6130 readonly occurrences: number
6131 readonly identifier: string | undefined
6132}
6133```
6134
6135Return a reference name to move the candidate into `references`, or return `undefined` to leave it inline. This example
6136extracts anonymous candidates only when the same candidate occurs more than once, while still respecting identifiers:
6137
6138```ts
6139import { Schema, SchemaRepresentation } from "effect"
6140
6141const Item = Schema.Struct({ name: Schema.String })
6142
6143const document = SchemaRepresentation.toRepresentations([Item.ast, Item.ast], {
6144 referencePolicy: ({ ast, identifier, occurrences }) => identifier ?? (occurrences > 1 ? `${ast._tag}_` : undefined)
6145})
6146
6147console.log(document.representations)
6148// [
6149// { _tag: "Reference", $ref: "Objects_" },
6150// { _tag: "Reference", $ref: "Objects_" }
6151// ]
6152```
6153
6154Occurrences are counted by candidate identity, not structural equality. Two independently constructed but structurally
6155equal ASTs are separate candidates and each reports one occurrence. Recursive candidates cannot remain inline: if a policy
6156returns `undefined` for one, the converter assigns a synthetic reference name. If different candidates request the same
6157name, later candidates receive numeric suffixes.
6158
6159`Schema.toJsonSchemaDocument(schema, options)` forwards the policy after deriving the schema's canonical JSON codec, so
6160the policy receives canonical JSON-encoded ASTs. `OpenApi.fromApi(api, options)` applies the policy at the same canonical
6161JSON boundary when deciding which schemas become OpenAPI components. A `referencePolicy` passed to
6162`SchemaRepresentation.toJsonSchemaDocument` or `toJsonSchemaMultiDocument` cannot change an existing document's references;
6163pass it while creating the `Document` or `MultiDocument` instead.
6164
6165## JSON persistence
6166
6167### `toJson` / `fromJson`
6168
6169`toJson(document)` projects and validates a live document, then returns a `Schema.Json` value suitable for storage or
6170transport. `fromJson(input)` validates persisted JSON and returns a `Document`; it does not restore runtime callbacks.
6171
6172The multi-root equivalents are `toJsonMultiDocument` and `fromJsonMultiDocument`.
6173
6174```ts
6175import { Schema, SchemaRepresentation } from "effect"
6176
6177const live = Schema.toRepresentation(
6178 Schema.String.check(Schema.isMinLength(3))
6179)
6180
6181const json = SchemaRepresentation.toJson(live)
6182const persisted = SchemaRepresentation.fromJson(json)
6183```
6184
6185Persisted `Declaration` and `Filter` nodes must contain a representation identity. `fromJson` validates the document but
6186does not require the corresponding revivers until reconstruction.
6187
6188## Rebuilding runtime schemas
6189
6190### `fromRepresentation`
6191
6192`fromRepresentation` rebuilds structural nodes, resolves references, restores recursion, reattaches annotations, and
6193reapplies checks. Revivers are resolved by `id`; none are installed implicitly, so the `revivers` array is required even
6194when it is empty.
6195
6196```ts
6197import { Schema, SchemaRepresentation } from "effect"
6198
6199const json = SchemaRepresentation.toJson(
6200 Schema.toRepresentation(
6201 Schema.String.check(Schema.isMinLength(3))
6202 )
6203)
6204
6205const document = SchemaRepresentation.fromJson(json)
6206const rebuilt = SchemaRepresentation.fromRepresentation(document, {
6207 revivers: [SchemaRepresentation.isMinLengthReviver]
6208})
6209
6210console.log(Schema.is(rebuilt)("abc"))
6211// true
6212console.log(Schema.is(rebuilt)("a"))
6213// false
6214```
6215
6216`SchemaRepresentation` exports individual revivers for built-in declarations and checks, such as
6217`OptionReviver`, `DateReviver`, and `isMinLengthReviver`. Supply every reviver required by the document; a missing or
6218duplicate `id`, or a payload that does not satisfy its reviver's `payloadSchema`, is an error.
6219
6220`fromRepresentations` rebuilds the ordered roots of a `MultiDocument` in a shared reference environment. Only references
6221reachable from those roots are revived.
6222
6223### Custom revivers
6224
6225There are separate reviver contracts for opaque declarations, leaf filters, and opaque filter groups:
6226
6227- `DeclarationReviver<P>`
6228- `FilterReviver<P>`
6229- `FilterGroupReviver<P>`
6230
6231Use `makeReviverDeclaration`, `makeReviverFilter`, and `makeReviverFilterGroup` to infer `P` from `payloadSchema`.
6232
6233```ts
6234import { Schema, SchemaRepresentation } from "effect"
6235
6236const id = "acme/schema/minLength"
6237
6238function minLength(
6239 minimum: number,
6240 annotations?: Schema.Annotations.Filter
6241) {
6242 return Schema.makeFilter<string>((value) => value.length >= minimum, {
6243 ...annotations,
6244 representation: { id, payload: { minimum } }
6245 })
6246}
6247
6248const minLengthReviver = SchemaRepresentation.makeReviverFilter(
6249 id,
6250 Schema.Struct({ minimum: Schema.Number }),
6251 ({ annotations, payload }) => minLength(payload.minimum, annotations)
6252)
6253```
6254
6255The same reviver can then be included in the `revivers` array passed to `fromRepresentation` or
6256`fromRepresentations`.
6257
6258## JSON Schema
6259
6260### Exporting JSON Schema
6261
6262For a runtime schema, prefer `Schema.toJsonSchemaDocument(schema)`. It first derives the schema's canonical JSON codec,
6263then compiles its encoded representation to JSON Schema Draft 2020-12. The default reference policy extracts candidates
6264with resolved identifiers and leaves anonymous non-recursive candidates inline, including repeated declarations. Recursive
6265candidates still receive references, using a synthetic name when necessary. Pass `referencePolicy` in the options to use a
6266different allocation rule.
6267
6268Generated JSON Schema is a preliminary validation layer. The Effect decoder
6269remains the final authority because string length, RegExp flags,
6270and decoded-object property checks can differ between the two validators.
6271If a `oneOf` branch contains a known approximation, the compiler emits `anyOf`.
6272This prevents the approximation from rejecting values by making multiple
6273branches match. Unions whose branches are all exact retain `oneOf`.
6274
6275The default `onExcessProperty: "ignore"` emits
6276`additionalProperties: true`; use `onExcessProperty: "error"` in both
6277generation and decoding to reject unmatched properties whenever the key space
6278has an exact selector. For `Schema.Record(Key, Value)`, a `patternProperties`
6279selector must represent the key check exactly. A looser pattern could select
6280extra keys and impose `Value` constraints on properties the Effect decoder ignores.
6281
6282When a key cannot be translated to an exact selector, including conjunctive key
6283patterns that the compiler does not merge into a single regular expression:
6284
6285- With `onExcessProperty: "ignore"`, the compiler omits that index-signature constraint.
6286- With `onExcessProperty: "error"`, it emits the generated key schemas under
6287 `propertyNames`, also allowing explicitly declared property names. Properties
6288 not selected by `properties` or exact `patternProperties` selectors may satisfy
6289 any candidate index-signature value schema. The Effect decoder enforces the
6290 exact association between keys and values.
6291
6292Exact selectors from other index signatures remain active. An exact key selector
6293is also retained when only its value schema is approximate.
6294
6295At the lower level, `SchemaRepresentation.toJsonSchemaDocument(document)` compiles a live `Document`, and
6296`toJsonSchemaMultiDocument` compiles a live `MultiDocument`. Check-level `toJsonSchema` callbacks contribute JSON Schema
6297constraints. Opaque declarations that have not been structurally lowered compile to an unconstrained JSON Schema.
6298Callback authors are responsible for the semantics of their output.
6299
6300#### Check exporters and approximation
6301
6302Check-level `toJsonSchema` callbacks return `SchemaRepresentation.ToJsonSchema.CheckOutput`:
6303
6304| Return value | Meaning |
6305| ---------------- | ---------------------------------------------------------------------------- |
6306| `schema` | The fragment represents the check exactly. |
6307| `[schema, true]` | The fragment accepts every value accepted by the check, and may accept more. |
6308
6309Use the tightest safe approximation available. Return `[{}, true]` when the
6310constraint must be omitted. Returning a bare `{}` instead declares that the
6311check imposes no constraint. The compiler trusts this declaration; it does not
6312prove that a custom export is exact or safely looser.
6313
6314Approximation propagates through grouped checks, array elements, object
6315properties, unions, and references, including recursive definitions. It also
6316propagates from dependencies listed in a check's `representation.schemas`, even
6317if its callback returns a plain fragment. The callback receives those compiled
6318dependencies in its `schemas` input. Filters without a `toJsonSchema` callback
6319and opaque declarations without a structural codec are treated as approximate.
6320
6321This information controls the `oneOf` and record-key fallbacks described above.
6322It is used during compilation and adds no metadata to the emitted JSON Schema.
6323
6324`toJsonSchema` callbacks must treat their input schemas as immutable and return a valid JSON Schema fragment, either
6325directly or in the tuple above. After a callback returns, it must not mutate that fragment or anything reachable from it;
6326returning a new graph is the supported way to produce different output during a later compilation. The compiler may cache structural comparisons while
6327deduplicating completed definitions, so mutating a previously returned graph can make equality results stale.
6328
6329Definitions are compared only with definitions in the same internal fallback-identifier group. Equal definitions in
6330different groups and definitions with explicit identifiers remain distinct. After compilation, local `#/$defs/...`
6331references are rewritten to the surviving definition, including references returned directly by callbacks. External
6332references and other local JSON Pointers remain unchanged.
6333
6334Because compiler callbacks are not persisted, compile the live document before calling `toJson`, or rebuild and lower the
6335schema with revivers first.
6336
6337### Importing JSON Schema
6338
6339`SchemaRepresentation.fromJsonSchemaDocument` imports a JSON Schema Draft 2020-12 document as a runtime `Schema.Top`.
6340It does not return a representation document.
6341
6342The input is assumed to be a valid Draft 2020-12 document and is not validated
6343against the meta-schema. Instance validation assumes JSON-compatible JavaScript
6344values produced by `JSON.parse`.
6345
6346Only direct local references to top-level definitions in the form `#/$defs/<escaped-token>` are supported. Root
6347references, external references, and pointers below a definition are rejected with the supported reference format.
6348Missing definitions are reported by name. References reached inside a nested schema
6349resource introduced by `$id` are rejected with their path instead of being resolved against the top-level definitions.
6350A root `$id`, or a nested `$id` without reached references, is supported.
6351
6352Import errors explain which constraint or reference cannot be translated and include its source path.
6353
6354`fromJsonSchemaMultiDocument` returns the ordered root schemas. It translates only definitions reachable from those
6355roots. To pass the result to a representation compiler, call `toRepresentations` with the returned schemas' ASTs.
6356
6357Import translates `{ not: {} }` as `Never`, including in optional properties. Other uses of `not` are unsupported.
6358
6359Import reuses Effect schemas and built-in checks. It is a best-effort translation, not a guarantee of identical
6360validation or a lossless round trip:
6361
6362- `additionalProperties: true`, `{}`, or an omitted `additionalProperties` imports additional values as `Schema.Json`.
6363 These properties are validated and retained, including with `onExcessProperty: "error"`.
6364- Closed objects normally have no catch-all for additional properties. The decoder strips excess properties by default;
6365 use `onExcessProperty: "error"` to reject them. Closed empty objects instead import as
6366 `Schema.Record(Schema.String, Schema.Never)` and reject string-keyed entries regardless of that option.
6367 A closed object with a single
6368 `patternProperties` entry and no named or required properties imports as a filtered `Schema.Record` when patterns
6369 are applied. Object keyword scopes still constrain declared properties when intersecting schemas.
6370 Combinations requiring an index signature to exclude
6371 explicit properties or patterned keys remain unsupported and are rejected with an explanation of the limitation.
6372- With `patterns: "apply"`, open patterned objects are also rejected. Their filtered
6373 index and catch-all would produce incompatible TypeScript index signatures. This includes `additionalProperties`
6374 set to `true`, `{}`, or omitted. An intersection with a closed object can still be imported when it reduces the
6375 result to a finite set of properties.
6376- `minProperties`, `maxProperties`, and `propertyNames` use the existing checks on the decoded object, after excess
6377 properties have been stripped. No separate validation of the original object is added.
6378- String `minLength` and `maxLength` count Unicode code points through `Schema.isMinCodePoints` and
6379 `Schema.isMaxCodePoints`, except that `minLength: 1` uses the equivalent non-empty check `Schema.isMinLength(1)`.
6380- `integer` uses `Schema.isInt` and rejects integers outside JavaScript's safe integer range.
6381- Applied patterns use `Schema.isPattern` with the `u` flag. Patterns that cannot be compiled in Unicode mode are
6382 rejected as unsupported translations, with their source path, rather than interpreted with different semantics.
6383
6384An Effect struct exported with `additionalProperties: true` therefore imports with a JSON-valued index signature,
6385even if the original struct had none. The imported decoder retains additional properties that the original decoder
6386would strip. Use the imported schema's actual checks and parse options when reasoning about validation.
6387
6388Import translates a Draft 2020-12 subset. `$dynamicRef`, `contains`, `dependentRequired`, `dependentSchemas`,
6389active `if` / `then` / `else`, `unevaluatedItems`, and `unevaluatedProperties` are rejected with an error identifying
6390the unsupported keyword. Inactive conditional keywords and `minContains` / `maxContains` without
6391`contains` have no validation effect and are ignored. Unknown extension keywords are ignored and their semantics are not
6392enforced. Objects and arrays used as `const` values or `enum` members are rejected. Only strings, numbers, booleans, and
6393null are supported. The optional `onEnter` callback can normalize each JSON Schema node
6394before it is translated.
6395
6396Intersections of overlapping unions are limited to disjoint root-type partitions and finite primitive `anyOf` literal
6397sets. Other union intersections, including cases that would duplicate a nested choice, are rejected.
6398
6399Regular expression constraints reached during translation are rejected by default because imported patterns use the
6400runtime's native regular expression engine and may block validation for an unbounded amount of time. Set
6401`patterns: "apply"` only for trusted documents. Set `patterns: "ignore"` to skip reached pattern constraints explicitly.
6402This can admit values the source rejects, but it can also reject previously valid values inside `oneOf` when removing
6403constraints makes multiple branches match. The policy includes `pattern`, the keys of
6404`patternProperties`, and patterns nested in `propertyNames`. Ignoring `patternProperties` also skips its value constraints
6405and `additionalProperties`, because matching keys cannot be determined without evaluating the patterns.
6406
6407## Code generation
6408
6409### `toCodeDocument`
6410
6411`toCodeDocument` compiles a live `MultiDocument` into runtime and TypeScript source fragments. It:
6412
6413- returns one `Code` value for each root
6414- sorts non-recursive references in dependency order
6415- keeps recursive references separate so callers can emit `Schema.suspend`
6416- sanitizes reference names into valid JavaScript identifiers
6417- collects symbol, enum, and import artifacts
6418
6419Opaque declarations and checks provide code through their `toCode` callbacks. `toCodeDocument` does not accept a
6420reviver option. To generate code from persisted JSON, first reconstruct the schemas with `fromRepresentation` or
6421`fromRepresentations`, then create a new live representation so the revivers can restore the callbacks.
6422
6423# Parsing Options
6424
6425## Concurrent Product Parsing
6426
6427The `concurrency` parse option controls how many children of a product schema may parse at the same time. It uses the
6428same semantics as `Effect.forEach`: `undefined` and `1` are sequential, a number greater than `1` sets a bound, and
6429`"unbounded"` removes the bound. Synchronous children remain eager and do not require a fiber.
6430
6431The option applies to tuple elements, array elements, struct fields, record entries, and structs with rest. It applies
6432independently at every nested product. For example, with `concurrency: 2`, two outer array elements may each parse two
6433inner elements concurrently. The option is runtime-only; it is not stored in the schema AST or its representation.
6434
6435Union members remain sequential. A product inside the union member currently being evaluated still receives the
6436option.
6437
6438**Example** (Decoding array elements concurrently)
6439
6440```ts
6441import { Effect, Schema, SchemaGetter } from "effect"
6442
6443const item = Schema.String.pipe(Schema.decode({
6444 decode: SchemaGetter.transformEffect((value) => Effect.sleep("10 millis").pipe(Effect.as(value))),
6445 encode: SchemaGetter.passthrough()
6446}))
6447
6448const program = Schema.decodeUnknownEffect(Schema.Array(item))(
6449 ["a", "b", "c"],
6450 { concurrency: 2 }
6451)
6452
6453const result = await Effect.runPromise(program)
6454// ["a", "b", "c"]
6455```
6456
6457Tuple and array results retain their element positions, as the result of `Effect.forEach` does. Other observable work
6458follows completion order. With `errors: "first"`, the first observed child failure terminates the product and interrupts
6459the remaining children. With `errors: "all"`, issues are accumulated as children complete. If transformed record keys
6460collide, the last assignment to complete wins. Concurrent work may already have performed effects when another child
6461fails or the parser interrupts it.
6462
6463# Error Handling and Formatting
6464
6465When validation fails, Schema produces structured error objects that describe what went wrong. Formatters turn those error objects into human-readable messages you can display to users or write to logs.
6466
6467### Reporting Rejected Inputs
6468
6469By default, schema issues neither retain rejected input values nor include them in formatted messages. Pass `{ reportInput: true }` to a parser when the additional diagnostic context is worth the disclosure and retention risk:
6470
6471```ts
6472import { Result, Schema, SchemaIssue, SchemaParser } from "effect"
6473
6474const result = SchemaParser.decodeUnknownResult(Schema.String)(1, { reportInput: true })
6475const formatIssue = SchemaIssue.makeFormatterDefault()
6476
6477if (Result.isFailure(result)) {
6478 SchemaIssue.hasInput(result.failure) // true
6479 if (SchemaIssue.hasInput(result.failure)) {
6480 result.failure.input // 1
6481 }
6482 formatIssue(result.failure) // "Expected string, got 1"
6483}
6484```
6485
6486Value-bearing issues created by the parser then expose an enumerable own `input` field. The input is retained by reference, not copied. Use `SchemaIssue.hasInput(issue)` instead of checking `issue.input !== undefined`, because a present input whose value is `undefined` is distinct from an issue that does not retain input.
6487
6488Enabling this option can retain or disclose secrets, personally identifiable information, and large object graphs. Object enumeration, spread, serialization, `SchemaIssue.makeFormatterDefault()`, `SchemaError.message`, and Standard Schema messages may expose the retained value. A Standard Schema failure still contains only its standard `message` and `path` fields; the input can appear inside `message`, but no non-standard `input` field is added.
6489
6490User-created `SchemaIssue.Issue` values returned directly by declarations, checks, transformations, or middleware are not modified. To make a custom value-bearing issue honor `reportInput`, pass the callback's input and effective parse options to its constructor, for example `new SchemaIssue.InvalidValue(annotations, input, options)`.
6491
6492### Formatters
6493
6494#### StandardSchemaV1 formatter
6495
6496The StandardSchemaV1 formatter is used by `Schema.toStandardSchemaV1` and will return a `StandardSchemaV1.FailureResult` object:
6497
6498```ts
6499export interface FailureResult {
6500 /** The issues of failed validation. */
6501 readonly issues: ReadonlyArray<Issue>
6502}
6503
6504export interface Issue {
6505 /** The error message of the issue. */
6506 readonly message: string
6507 /** The path of the issue. */
6508 readonly path: ReadonlyArray<PropertyKey>
6509}
6510```
6511
6512You can customize the messages of the `Issue` object in two main ways:
6513
6514- By passing formatter hooks
6515- By annotating schemas with `message` or `messageMissingKey` or `messageUnexpectedKey`
6516
6517For the exact rule used by the default formatter for identifiers, filter
6518`expected`, and `message` annotations, see
6519[Filter error messages and schema identifiers](#filter-error-messages-and-schema-identifiers).
6520
6521##### Hooks
6522
6523Formatter hooks let you define custom messages in one place and apply them across different schemas. This can help avoid repeating message definitions and makes it easier to update them later.
6524
6525Hooks are **required**. There is a default implementation that can be overridden only for demo purposes. This design helps keep the bundle size smaller by avoiding unused message formatting logic.
6526
6527There are two kinds of hooks:
6528
6529- `LeafHook` — for issues that occur at leaf nodes in the schema.
6530- `CheckHook` — for custom validation checks.
6531
6532`LeafHook` handles these issue types:
6533
6534- `InvalidType`
6535- `InvalidValue`
6536- `MissingKey`
6537- `UnexpectedKey`
6538- `Forbidden`
6539- `OneOf`
6540
6541`CheckHook` handles `Check` issues, such as failed filters / refinements.
6542
6543**Example** (Default hooks)
6544
6545Default hooks are just for demo purposes:
6546
6547- LeafHook: returns the issue tag
6548- CheckHook: returns the meta infos of the check as a string
6549
6550```ts
6551import { Effect, Schema, SchemaIssue } from "effect"
6552
6553const schema = Schema.Struct({
6554 a: Schema.NonEmptyString,
6555 b: Schema.NonEmptyString
6556})
6557
6558Schema.decodeUnknownEffect(schema)({ b: "" }, { errors: "all" })
6559 .pipe(
6560 Effect.mapError((error) => SchemaIssue.makeFormatterStandardSchemaV1()(error.issue)),
6561 Effect.runPromise
6562 )
6563 .then(console.log, (a) => console.dir(a, { depth: null }))
6564/*
6565Output:
6566{
6567 issues: [
6568 { path: [ 'a' ], message: 'Missing key' },
6569 { path: [ 'b' ], message: 'Expected a value with a length of at least 1' }
6570 ]
6571}
6572*/
6573```
6574
6575##### Customizing messages
6576
6577If a schema has a `message` annotation, it will take precedence over any formatter hook.
6578
6579To make the examples easier to follow, we define a helper function that prints formatted validation messages using `SchemaFormatter`.
6580
6581**Example utilities**
6582
6583```ts
6584// utils.ts
6585import { Exit, Schema, SchemaIssue } from "effect"
6586import i18next from "i18next"
6587
6588i18next.init({
6589 lng: "en",
6590 resources: {
6591 en: {
6592 translation: {
6593 "string.mismatch": "Please enter a valid string",
6594 "string.minLength": "Please enter at least {{minLength}} character(s)",
6595 "struct.missingKey": "This field is required",
6596 "struct.mismatch": "Please enter a valid object",
6597 "default.mismatch": "Invalid type",
6598 "default.invalidValue": "Invalid value",
6599 "default.forbidden": "Forbidden operation",
6600 "default.oneOf": "Too many successful values",
6601 "default.check": "The value does not match the check"
6602 }
6603 }
6604 }
6605})
6606
6607export const t = i18next.t
6608
6609export function getLogIssues(options?: {
6610 readonly leafHook?: SchemaIssue.LeafHook | undefined
6611 readonly checkHook?: SchemaIssue.CheckHook | undefined
6612}) {
6613 return <S extends Schema.Codec<unknown, unknown, never, never>>(schema: S, input: unknown) => {
6614 console.log(
6615 String(
6616 Schema.decodeUnknownExit(schema)(input, { errors: "all" }).pipe(
6617 Exit.mapError((err) => SchemaIssue.makeFormatterStandardSchemaV1(options)(err.issue).issues)
6618 )
6619 )
6620 )
6621 }
6622}
6623```
6624
6625**Example** (Using hooks to translate common messages)
6626
6627```ts
6628import { Schema } from "effect"
6629import { getLogIssues, t } from "./utils.js"
6630
6631const Person = Schema.Struct({
6632 name: Schema.String.check(Schema.isNonEmpty())
6633})
6634
6635// Configure hooks to customize how issues are rendered
6636const logIssues = getLogIssues({
6637 // Format leaf-level issues (missing key, wrong type, etc.)
6638 leafHook: (issue) => {
6639 switch (issue._tag) {
6640 case "InvalidType": {
6641 if (issue.ast._tag === "String") {
6642 return t("string.mismatch") // Wrong type for a string
6643 } else if (issue.ast._tag === "Objects") {
6644 return t("struct.mismatch") // Value is not an object
6645 }
6646 return t("default.mismatch") // Fallback for other types
6647 }
6648 case "InvalidValue": {
6649 return t("default.invalidValue")
6650 }
6651 case "MissingKey":
6652 return t("struct.missingKey")
6653 case "UnexpectedKey":
6654 return t("struct.unexpectedKey")
6655 case "Forbidden":
6656 return t("default.forbidden")
6657 case "OneOf":
6658 return t("default.oneOf")
6659 }
6660 },
6661 // Format custom check errors (like isMinLength or user-defined validations)
6662 checkHook: (issue) => {
6663 const meta = issue.filter.annotations?.meta
6664 if (meta) {
6665 switch (meta._tag) {
6666 case "isMinLength": {
6667 return t("string.minLength", { minLength: meta.minLength })
6668 }
6669 }
6670 }
6671 return t("default.check")
6672 }
6673})
6674
6675// Invalid object (not even a struct)
6676logIssues(Person, null)
6677// Failure(Cause([Fail([{"path":[],"message":"Please enter a valid object"}])]))
6678
6679// Missing "name" key
6680logIssues(Person, {})
6681// Failure(Cause([Fail([{"path":["name"],"message":"This field is required"}])]))
6682
6683// "name" has the wrong type
6684logIssues(Person, { name: 1 })
6685// Failure(Cause([Fail([{"path":["name"],"message":"Please enter a valid string"}])]))
6686
6687// "name" is an empty string
6688logIssues(Person, { name: "" })
6689// Failure(Cause([Fail([{"path":["name"],"message":"Please enter at least 1 character(s)"}])]))
6690```
6691
6692##### Inline custom messages
6693
6694You can attach custom error messages directly to a schema using annotations. These messages can either be plain strings or functions that return strings. This is useful when you want to provide field-specific wording or localization without relying on formatter hooks.
6695
6696**Example** (Attaching custom messages to a struct field)
6697
6698```ts
6699import { Schema } from "effect"
6700import { getLogIssues, t } from "./utils.js"
6701
6702const Person = Schema.Struct({
6703 name: Schema.String
6704 // Message for invalid type (e.g., number instead of string)
6705 .annotate({ message: t("string.mismatch") })
6706 // Message to show when the key is missing
6707 .annotateKey({ messageMissingKey: t("struct.missingKey") })
6708 // Message to show when the string is empty
6709 .check(Schema.isNonEmpty({ message: t("string.minLength", { minLength: 1 }) }))
6710})
6711 // Message to show when the whole object has the wrong shape
6712 .annotate({ message: t("struct.mismatch") })
6713
6714// Use defaults for leaf and check hooks
6715const logIssues = getLogIssues()
6716
6717// Invalid object (not even a struct)
6718logIssues(Person, null)
6719// Failure(Cause([Fail([{"path":[],"message":"Please enter a valid object"}])]))
6720
6721// Missing "name" key
6722logIssues(Person, {})
6723// Failure(Cause([Fail([{"path":["name"],"message":"This field is required"}])]))
6724
6725// "name" has the wrong type
6726logIssues(Person, { name: 1 })
6727// Failure(Cause([Fail([{"path":["name"],"message":"Please enter a valid string"}])]))
6728
6729// "name" is an empty string
6730logIssues(Person, { name: "" })
6731// Failure(Cause([Fail([{"path":["name"],"message":"Please enter at least 1 character(s)"}])]))
6732```
6733
6734##### Sending a FailureResult over the wire
6735
6736You can use the `Schema.StandardSchemaV1FailureResult` schema to send a `StandardSchemaV1.FailureResult` over the wire.
6737
6738**Example** (Sending a FailureResult over the wire)
6739
6740```ts
6741import { Schema, SchemaIssue, SchemaParser } from "effect"
6742
6743const b = Symbol.for("b")
6744
6745const schema = Schema.Struct({
6746 a: Schema.NonEmptyString,
6747 [b]: Schema.Finite,
6748 c: Schema.Tuple([Schema.String])
6749})
6750
6751const r = SchemaParser.decodeUnknownExit(schema)({ a: "", c: [] }, { errors: "all" })
6752
6753if (r._tag === "Failure") {
6754 const failures = r.cause.failures
6755 if (failures[0]?._tag === "Fail") {
6756 const failureResult = SchemaIssue.makeFormatterStandardSchemaV1()(failures[0].error)
6757 const serializer = Schema.toCodecJson(Schema.StandardSchemaV1FailureResult)
6758 console.dir(Schema.encodeSync(serializer)(failureResult), { depth: null })
6759 }
6760}
6761/*
6762{
6763 issues: [
6764 {
6765 message: 'Expected a value with a length of at least 1',
6766 path: [ 'a' ]
6767 },
6768 { message: 'Missing key', path: [ 'c', 0 ] },
6769 { message: 'Missing key', path: [ 'Symbol(b)' ] }
6770 ]
6771}
6772*/
6773```
6774
6775# Middlewares
6776
6777A middleware wraps around the decoding or encoding process, letting you intercept errors, provide fallback values, or inject services. The most common use case is returning a default value when decoding fails.
6778
6779## Fallbacks
6780
6781You can use `Schema.catchDecoding` to return a fallback value when decoding fails.
6782This API uses an Effect without a context. If you need a fallback value that depends on a service, use `Schema.catchDecodingWithContext`.
6783
6784**Example** (Returning a simple fallback value)
6785
6786```ts
6787import { Effect, Schema } from "effect"
6788
6789// Provide a fallback string when decoding does not succeed
6790const schema = Schema.String.pipe(Schema.catchDecoding(() => Effect.succeedSome("b")))
6791
6792console.log(String(Schema.decodeUnknownExit(schema)(null)))
6793// Success("b")
6794```
6795
6796You can also return `Option.none()` to omit a field from the output.
6797This is useful when working with optional fields.
6798
6799**Example** (Omitting a field when decoding fails)
6800
6801```ts
6802import { Effect, Schema } from "effect"
6803
6804// Omit the field when decoding does not succeed
6805const schema = Schema.Struct({
6806 a: Schema.optionalKey(Schema.String).pipe(Schema.catchDecoding(() => Effect.succeedNone))
6807})
6808
6809console.log(String(Schema.decodeUnknownExit(schema)({ a: null })))
6810// Success({})
6811```
6812
6813### Using a Service to provide a fallback value
6814
6815You can use `Schema.catchDecodingWithContext` to get a fallback value from a service.
6816
6817**Example** (Retrieving a fallback value from a service)
6818
6819```ts
6820import { Context, Effect, Option, Schema } from "effect"
6821
6822// Define a service that provides a fallback value
6823class Service extends Context.Service<Service, { fallback: Effect.Effect<string> }>()("Service") {}
6824
6825// ┌─── Codec<string, string, Service, never>
6826// ▼
6827const schema = Schema.revealCodec(
6828 Schema.revealCodec(
6829 Schema.String.pipe(
6830 Schema.catchDecodingWithContext(() =>
6831 Effect.gen(function*() {
6832 const service = yield* Service
6833 return Option.some(yield* service.fallback)
6834 })
6835 )
6836 )
6837 )
6838)
6839
6840// Provide the service during decoding
6841// ┌─── Codec<string, string, never, never>
6842// ▼
6843const provided = Schema.revealCodec(
6844 schema.pipe(Schema.middlewareDecoding(Effect.provideService(Service, { fallback: Effect.succeed("b") })))
6845)
6846
6847console.log(String(Schema.decodeUnknownExit(provided)(null)))
6848// Success("b")
6849```
6850
6851# Advanced Topics
6852
6853This section covers Schema's internal type machinery and advanced features. You don't need this to use Schema — it's here for library authors and advanced users who want to understand or extend the type system.
6854
6855## Model
6856
6857A "schema" is a strongly typed wrapper around an untyped AST (abstract syntax tree) node.
6858
6859The base interface is `Bottom`, which sits at the bottom of the schema type hierarchy. In Schema v4, the number of tracked type parameters has increased to 15, allowing for more precise and flexible schema definitions.
6860
6861```ts
6862export interface Bottom<
6863 out T,
6864 out E,
6865 out RD,
6866 out RE,
6867 out Ast extends AST.AST,
6868 out RebuildOut extends Top,
6869 out TypeMakeIn = T,
6870 out Iso = T,
6871 in out TypeParameters extends ReadonlyArray<Top> = readonly [],
6872 out TypeMake = TypeMakeIn,
6873 out TypeMutability extends Mutability = "readonly",
6874 out TypeOptionality extends Optionality = "required",
6875 out TypeConstructorDefault extends ConstructorDefault = "no-default",
6876 out EncodedMutability extends Mutability = "readonly",
6877 out EncodedOptionality extends Optionality = "required"
6878> extends Pipeable.Pipeable {
6879 readonly [TypeId]: typeof TypeId
6880
6881 readonly ast: Ast
6882 readonly "Rebuild": RebuildOut
6883 readonly "~type.parameters": TypeParameters
6884
6885 readonly Type: T
6886 readonly Encoded: E
6887 readonly DecodingServices: RD
6888 readonly EncodingServices: RE
6889
6890 readonly "~type.make.in": TypeMakeIn
6891 readonly "~type.make": TypeMake // useful to type the `refine` interface
6892 readonly "~type.constructor.default": TypeConstructorDefault
6893 readonly Iso: Iso
6894
6895 readonly "~type.mutability": TypeMutability
6896 readonly "~type.optionality": TypeOptionality
6897 readonly "~encoded.mutability": EncodedMutability
6898 readonly "~encoded.optionality": EncodedOptionality
6899
6900 annotate(annotations: Annotations.Bottom<this["Type"], this["~type.parameters"]>): this["Rebuild"]
6901 annotateKey(annotations: Annotations.Key<this["Type"]>): this["Rebuild"]
6902 check(...checks: readonly [AST.Check<this["Type"]>, ...Array<AST.Check<this["Type"]>>]): this["Rebuild"]
6903 rebuild(ast: this["ast"]): this["Rebuild"]
6904 /**
6905 * @throws {Error} The issue is contained in the error cause.
6906 */
6907 make(input: this["~type.make.in"], options?: MakeOptions): this["Type"]
6908}
6909```
6910
6911### Parameter Overview
6912
6913- `T`: the decoded output type
6914- `E`: the encoded representation
6915- `RD`: the type of the services required for decoding
6916- `RE`: the type of the services required for encoding
6917- `Ast`: the AST node type
6918- `RebuildOut`: the type returned when modifying the schema (namely when you add annotations or checks)
6919- `TypeMakeIn`: the type of the input to the `make` constructor
6920- `Iso`: the type of the focus of the default `Optic.Iso`
6921- `TypeParameters`: the type of the type parameters
6922
6923Contextual information about the schema (when the schema is used in a composite schema such as a struct or a tuple):
6924
6925- `TypeMake`: the type used to construct the value
6926- `TypeReadonly`: whether the schema is readonly on the type side
6927- `TypeIsOptional`: whether the schema is optional on the type side
6928- `TypeDefault`: whether the constructor has a default value
6929- `EncodedIsReadonly`: whether the schema is readonly on the encoded side
6930- `EncodedIsOptional`: whether the schema is optional on the encoded side
6931
6932### AST Node Structure
6933
6934Every schema is based on an AST node with a consistent internal shape:
6935
6936```mermaid
6937classDiagram
6938 class ASTNode {
6939 + annotations
6940 + checks
6941 + encoding
6942 + context
6943 + ...specific node fields...
6944 }
6945```
6946
6947- `annotations`: metadata attached to the schema node
6948- `checks`: an array of validation rules
6949- `encoding`: a list of transformations that describe how to encode the value
6950- `context`: includes details used when the schema appears inside composite schemas such as structs or tuples (e.g., whether the field is optional or mutable)
6951
6952## Type Hierarchy
6953
6954The `Bottom` type is the foundation of the schema system. It carries all internal type parameters used by the library.
6955
6956Higher-level schema types build on this base by narrowing those parameters. Common derived types include:
6957
6958- `Top`: a generic schema with no fixed shape
6959- `Schema<T>`: represents the TypeScript type `T`
6960- `Codec<T, E, RD, RE>`: a schema that decodes `E` to `T` and encodes `T` to `E`, possibly requiring services `RD` and `RE`
6961
6962```mermaid
6963flowchart TD
6964 T[Top] --> S["Schema[T]"]
6965 S --> C["Codec[T, E, RD, RE]"]
6966 S --> O["Optic[T, Iso]"]
6967 C --> B["Bottom[T, E, RD, RE, Ast, RebuildOut, TypeMakeIn, Iso, TypeParameters, TypeMake, TypeMutability, TypeOptionality, TypeConstructorDefault, EncodedMutability, EncodedOptionality]"]
6968```
6969
6970### Best Practices
6971
6972Use `Top`, `Schema`, and `Codec` as _constraints_ only. Do not use them as explicit annotations or return types.
6973
6974**Example** (Prefer constraints over wide annotations)
6975
6976```ts
6977import { Schema } from "effect"
6978
6979// ✅ Use as a constraint. S can be any schema that extends Top.
6980declare function foo<S extends Schema.Top>(schema: S)
6981
6982// ❌ Do not return Codec directly. It erases useful type information.
6983declare function bar(): Schema.Codec<number, string>
6984
6985// ❌ Avoid wide annotations that lose details baked into a specific schema.
6986const schema: Schema.Codec<number, string> = Schema.FiniteFromString
6987```
6988
6989These wide types reset other internal parameters to defaults, which removes useful information:
6990
6991- `Top`: all type parameters are set to defaults
6992- `Schema`: all type parameters except `Type` are set to defaults
6993- `Codec`: all type parameters except `Type`, `Encoded`, `DecodingServices`, `EncodingServices` are set to defaults
6994
6995**Example** (How wide annotations erase information)
6996
6997```ts
6998import { Schema } from "effect"
6999
7000// Read a hidden type-level property from a concrete schema
7001type TypeMutability = (typeof Schema.FiniteFromString)["~type.mutability"] // "readonly"
7002
7003const schema: Schema.Codec<number, string> = Schema.FiniteFromString
7004
7005// After widening to Codec<...>, the mutability info is broadened
7006type TypeMutability2 = (typeof schema)["~type.mutability"] // "readonly" | "mutable"
7007```
7008
7009## Typed Annotations
7010
7011You can retrieve typed annotations with the `Schema.resolveAnnotations` function. The function is called "resolve" rather than "get" because it performs a lookup: if the schema has checks, the annotations are taken from the last check; otherwise they are taken from the base schema instance. This means annotations placed on a check (e.g. via `.check(myCheck.annotate({ ... }))`) take precedence over annotations on the schema itself.
7012
7013**Example** (Resolving annotations from a base schema)
7014
7015```ts
7016import { Schema } from "effect"
7017
7018const schema = Schema.String.annotate({ title: "my string" })
7019
7020console.log(Schema.resolveAnnotations(schema))
7021// Output: { title: "my string" }
7022```
7023
7024**Example** (Annotations on the last check take precedence)
7025
7026```ts
7027import { Schema } from "effect"
7028
7029const schema = Schema.String
7030 .annotate({ title: "base" })
7031 .check(Schema.isNonEmpty().annotate({ title: "from check" }))
7032
7033console.log(Schema.resolveAnnotations(schema)?.title)
7034// Output: "from check"
7035```
7036
7037You can also extend the available annotations by adding your own in a module declaration file.
7038
7039**Example** (Adding a custom annotation for versioning)
7040
7041```ts
7042import { Schema } from "effect"
7043
7044// Extend the Annotations interface with a custom `version` annotation
7045declare module "effect/Schema" {
7046 namespace Annotations {
7047 interface Augment {
7048 readonly version?: readonly [major: number, minor: number, patch: number] | undefined
7049 }
7050 }
7051}
7052
7053// The `version` annotation is now recognized by the TypeScript compiler
7054const schema = Schema.String.annotate({ version: [1, 2, 0] })
7055
7056// const version: readonly [major: number, minor: number, patch: number] | undefined
7057const version = Schema.resolveAnnotations(schema)?.["version"]
7058
7059if (version) {
7060 // Access individual parts of the version
7061 console.log(version[1])
7062 // Output: 2
7063}
7064```
7065
7066### Key-level Annotations
7067
7068Key-level annotations are attached via `annotateKey` and apply to a field's position inside a `Struct` or `Tuple` rather than to the field's value type. Use `Schema.resolveAnnotationsKey` to retrieve them.
7069
7070**Example** (Resolving key-level annotations)
7071
7072```ts
7073import { Schema } from "effect"
7074
7075const schema = Schema.String.annotateKey({ messageMissingKey: "required" })
7076
7077console.log(Schema.resolveAnnotationsKey(schema))
7078// Output: { messageMissingKey: "required" }
7079```
7080
7081## Generics Improvements
7082
7083Using generics in schema composition and filters can be difficult.
7084
7085The plan is to make generics **covariant** and easier to use.
7086
7087## Separate Requirement Type Parameters
7088
7089In real-world applications, decoding and encoding often have different dependencies. For example, decoding may require access to a database, while encoding does not.
7090
7091To support this, schemas now have two separate requirement parameters:
7092
7093```ts
7094interface Codec<T, E, RD, RE> {
7095 // ...
7096}
7097```
7098
7099- `RD`: services required **only for decoding**
7100- `RE`: services required **only for encoding**
7101
7102This makes it easier to work with schemas in contexts where one direction has no external dependencies.
7103
7104**Example** (Decoding requirements are ignored during encoding)
7105
7106```ts
7107import type { Effect } from "effect"
7108import { Context, Schema } from "effect"
7109
7110// A service that retrieves full user info from an ID
7111class UserDatabase extends Context.Service<
7112 UserDatabase,
7113 {
7114 getUserById: (id: string) => Effect.Effect<{ readonly id: string; readonly name: string }>
7115 }
7116>()("UserDatabase") {}
7117
7118// Schema that decodes from an ID to a user object using the database,
7119// but encodes just the ID
7120declare const User: Schema.Codec<
7121 { id: string; name: string },
7122 string,
7123 UserDatabase, // Decoding requires the database
7124 never // Encoding does not require any services
7125>
7126
7127// ┌─── Effect<{ readonly id: string; readonly name: string; }, Schema.SchemaError, UserDatabase>
7128// ▼
7129const decoding = Schema.decodeEffect(User)("user-123")
7130
7131// ┌─── Effect<string, Schema.SchemaError, never>
7132// ▼
7133const encoding = Schema.encodeEffect(User)({ id: "user-123", name: "John Doe" })
7134```
7135
7136# Integrations
7137
7138Schema integrates with popular frameworks and libraries. This section shows working examples for forms (TanStack Form) and web servers (Elysia).
7139
7140### Forms
7141
7142#### TanStack Form
7143
7144Features:
7145
7146- Errors are formatted with the `StandardSchemaV1` formatter.
7147- Fields are validated **and parsed** (not just strings).
7148- You can add form-level validation by attaching filters to the struct.
7149- Schemas may include async transformations.
7150
7151**Example** (Parse user input and surface form-level errors)
7152
7153```tsx
7154import { useForm } from "@tanstack/react-form"
7155import type { AnyFieldApi } from "@tanstack/react-form"
7156import { Effect, Schema, SchemaGetter, SchemaTransformation } from "effect"
7157import React from "react"
7158
7159// ----------------------------------------------------
7160// Toolkit
7161// ----------------------------------------------------
7162
7163// Treat an empty string from the UI as `undefined` for optional fields,
7164// and encode `undefined` back to an empty string when showing it.
7165const UndefinedFromEmptyString = Schema.Undefined.pipe(
7166 Schema.encodeTo(Schema.Literal(""), {
7167 decode: SchemaGetter.transform(() => undefined),
7168 encode: SchemaGetter.transform(() => "" as const)
7169 })
7170)
7171
7172// Helper to make any schema "UI-optional":
7173// - empty string -> undefined
7174// - otherwise validate/parse with the given schema
7175function optional<S extends Schema.Top>(schema: S) {
7176 return Schema.Union([UndefinedFromEmptyString, schema])
7177}
7178
7179// Decode helper that returns a `Promise<Result>` with either a typed value
7180// or a human-friendly error message string.
7181function decode<T, E>(schema: Schema.Codec<T, E>) {
7182 return function(value: unknown) {
7183 return Schema.decodeUnknownEffect(schema)(value).pipe(
7184 Effect.mapError((error) => error.message),
7185 Effect.result,
7186 Effect.runPromise
7187 )
7188 }
7189}
7190
7191// ----------------------------------------------------
7192// Schemas
7193// ----------------------------------------------------
7194
7195const FirstName = Schema.String.check(
7196 Schema.isMinLength(3, {
7197 message: "must be at least 3 characters"
7198 })
7199)
7200const Age = Schema.Number.check(
7201 Schema.isInt({ message: "must be an integer" }).abort(),
7202 Schema.isBetween(
7203 { minimum: 18, maximum: 100 },
7204 {
7205 message: "must be between 18 and 100"
7206 }
7207 )
7208).pipe(Schema.encodeTo(Schema.String, SchemaTransformation.numberFromString))
7209
7210// Whole-form schema with a form-level rule:
7211// If firstName is "John", age is required.
7212const schema = Schema.Struct({
7213 firstName: FirstName,
7214 age: optional(Age)
7215}).check(
7216 Schema.makeFilter(({ firstName, age }) => {
7217 if (firstName === "John" && age === undefined) return "Age is required for John"
7218 })
7219)
7220
7221function FieldInfo({ field }: { field: AnyFieldApi }) {
7222 return (
7223 <>
7224 {field.state.meta.isTouched && !field.state.meta.isValid ?
7225 <em>{field.state.meta.errors.map((error) => error.message).join(", ")}</em> :
7226 null}
7227 {field.state.meta.isValidating ? "Validating..." : null}
7228 </>
7229 )
7230}
7231
7232export default function App() {
7233 // We parse the whole form on submit and keep the typed value here
7234 const parsedRef = React.useRef<undefined | typeof schema.Type>(undefined)
7235
7236 const form = useForm({
7237 defaultValues: {
7238 firstName: "John",
7239 age: ""
7240 } satisfies (typeof schema)["Encoded"],
7241 validators: {
7242 onChangeAsync: Schema.toStandardSchemaV1(schema),
7243
7244 // Final guard before submit:
7245 // - decode the entire form
7246 // - on failure: return a string (form-level error)
7247 // - on success: stash the typed value for `onSubmit`
7248 onSubmitAsync: async ({ value }) => {
7249 const r = await decode(schema)(value)
7250 if (r._tag === "Failure") return r.failure
7251 parsedRef.current = r.success
7252 }
7253 },
7254
7255 // Submit runs only if validators pass.
7256 // At this point `parsedRef.current` holds the fully typed value.
7257 onSubmit: async () => {
7258 // get the parsed value from the ref
7259 const parsed = parsedRef.current
7260 if (!parsed) throw new Error("Unexpected submit without parsed data")
7261 // Use the typed data here (no post-processing needed)
7262 console.log(parsed)
7263 }
7264 })
7265
7266 return (
7267 <div>
7268 <h1>Simple Form Example</h1>
7269 <form
7270 onSubmit={(e) => {
7271 e.preventDefault()
7272 e.stopPropagation()
7273 form.handleSubmit()
7274 }}
7275 >
7276 <div>
7277 <form.Field name="firstName">
7278 {(field) => {
7279 return (
7280 <>
7281 <label htmlFor={field.name}>First Name:</label>
7282 <input
7283 id={field.name}
7284 name={field.name}
7285 value={field.state.value}
7286 onBlur={field.handleBlur}
7287 onChange={(e) => field.handleChange(e.target.value)}
7288 />
7289 <FieldInfo field={field} />
7290 </>
7291 )
7292 }}
7293 </form.Field>
7294 </div>
7295
7296 <div>
7297 <form.Field name="age">
7298 {(field) => {
7299 return (
7300 <>
7301 <label htmlFor={field.name}>Age:</label>
7302 <input
7303 id={field.name}
7304 name={field.name}
7305 value={field.state.value}
7306 onBlur={field.handleBlur}
7307 onChange={(e) => field.handleChange(e.target.value)}
7308 />
7309 <FieldInfo field={field} />
7310 </>
7311 )
7312 }}
7313 </form.Field>
7314 </div>
7315
7316 <form.Subscribe selector={(s) => [s.errorMap]}>
7317 {([errorMap]) =>
7318 errorMap.onSubmit ?
7319 (
7320 <div role="alert" style={{ marginTop: 12 }}>
7321 <em>{String(errorMap.onSubmit)}</em>
7322 </div>
7323 ) :
7324 null}
7325 </form.Subscribe>
7326
7327 <form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
7328 {([canSubmit, isSubmitting]) => (
7329 <button type="submit" disabled={!canSubmit}>
7330 {isSubmitting ? "..." : "Submit"}
7331 </button>
7332 )}
7333 </form.Subscribe>
7334 </form>
7335 </div>
7336 )
7337}
7338```
7339
7340### Integrations
7341
7342#### Elysia
7343
7344```ts
7345import { node } from "@elysiajs/node"
7346import { openapi } from "@elysiajs/openapi"
7347import { Schema } from "effect"
7348import { Elysia } from "elysia"
7349
7350// ----------------------------------------------------
7351// Utilities
7352// ----------------------------------------------------
7353
7354function encodingJsonSchema<T, E, RD>(schema: Schema.Codec<T, E, RD, never>) {
7355 return Schema.toStandardSchemaV1(
7356 Schema.flip(Schema.toCodecJson(schema)).annotate({
7357 direction: "encoding"
7358 })
7359 )
7360}
7361
7362function decodingJsonSchema<T, E, RE>(schema: Schema.Codec<T, E, never, RE>) {
7363 return Schema.toStandardSchemaV1(Schema.toCodecJson(schema))
7364}
7365
7366function decodingStringSchema<T, E, RE>(schema: Schema.Codec<T, E, never, RE>) {
7367 return Schema.toStandardSchemaV1(Schema.toCodecStringTree(schema))
7368}
7369
7370function mapJsonSchema(schema: Schema.Top) {
7371 return Schema.toJsonSchema(schema.ast.annotations?.direction === "encoding" ? Schema.flip(schema) : schema, {
7372 target: "draft-2020-12", // or "draft-07"
7373 referenceStrategy: "skip"
7374 }).schema
7375}
7376
7377// ----------------------------------------------------
7378// Application
7379// ----------------------------------------------------
7380
7381new Elysia({ adapter: node() })
7382 .use(
7383 openapi({
7384 mapJsonSchema: {
7385 effect: mapJsonSchema
7386 }
7387 })
7388 )
7389 .get(
7390 "/id/:id",
7391 async ({ status, params, query }) => {
7392 console.log(`params: ${JSON.stringify(params)}`)
7393 console.log(`query: ${JSON.stringify(query)}`)
7394 return status(200, { date: new Date() })
7395 },
7396 {
7397 params: decodingStringSchema(
7398 Schema.Struct({
7399 id: Schema.Int
7400 })
7401 ),
7402 query: decodingStringSchema(
7403 Schema.Struct({
7404 required: Schema.String,
7405 optional: Schema.optionalKey(Schema.String),
7406 array: Schema.Array(Schema.String),
7407 tuple: Schema.Tuple([Schema.String, Schema.Int])
7408 })
7409 ),
7410 response: {
7411 200: encodingJsonSchema(
7412 Schema.Struct({
7413 date: Schema.ValidDate
7414 })
7415 )
7416 }
7417 }
7418 )
7419 .post(
7420 "/body",
7421 ({ body }) => {
7422 console.log(body)
7423 return { bigint: body.bigint + 1n }
7424 },
7425 {
7426 body: decodingJsonSchema(
7427 Schema.Struct({
7428 bigint: Schema.BigInt
7429 })
7430 ),
7431 response: {
7432 200: encodingJsonSchema(
7433 Schema.Struct({
7434 bigint: Schema.BigInt
7435 })
7436 )
7437 }
7438 }
7439 )
7440 .listen(3000)
7441```