Schema Annotations
Schema AST nodes can carry optional metadata called annotations. Use the .annotate(...) method or Schema.annotate(...) for the decoded side, Schema.annotateEncoded(...) for the encoded side, and Schema.annotateKey(...) for a struct field or tuple element.
Example (Using Annotations to Customize Schema)
import { Schema } from "effect"
// Define a Password schema, starting with a string typeconst Password = Schema.String // Add a custom error message for non-string values .annotate({ message: "not a string" }) .pipe( // Enforce non-empty strings and provide a custom error message Schema.check( Schema.isNonEmpty({ message: "required" }), // Restrict the string length to 10 characters or fewer // with a custom error message for exceeding length Schema.makeFilter((s) => s.length <= 10 ? undefined : "must be at most 10 characters long", ), ), ) .annotate({ // Add a unique identifier for the schema identifier: "Password", // Provide a title for the schema title: "password", // Include a description explaining what this schema represents description: "A password is a secret string used to authenticate a user", // Add examples for better clarity examples: ["1Ki77y", "jelly22fi$h"], // Include any additional documentation documentation: `...technical information on Password schema...`, })Built-in Annotations
The available annotations depend on the kind of schema node. These are the most common ones:
| Annotation | Scope | Description |
|---|---|---|
identifier |
schema | Stable name used by schema interpreters, including JSON Schema references and expected-value messages. |
expected |
schema or check | Human-readable description used by the default error formatter. |
title |
schema or key | Short display title, also understood by JSON Schema tooling. |
description |
schema or key | Longer documentation for the represented value. |
documentation |
schema or key | Additional developer-facing documentation. |
examples |
schema or key | Example decoded values; this is metadata and does not validate them. |
default |
schema or key | A documented default value; it does not change decoding or construction behavior. |
message |
schema or check | Replaces the default message for matching failures. |
messageMissingKey |
key | Replaces the error message when a required key is absent. |
messageUnexpectedKey |
schema | Replaces the message for excess keys when onExcessProperty is "error". |
parseOptions |
schema | Overrides parse options for that schema node. |
toJsonSchema |
check | Describes a custom check to the JSON Schema interpreter. |
toArbitrary |
schema or declaration | Customizes Arbitrary generation. |
toFormatter |
declaration | Defines Formatter behavior for a custom declaration. |
toEquivalence |
declaration | Defines Equivalence behavior for a custom declaration. |
toCodecJson |
declaration | Defines how a custom declaration is represented by the JSON codec interpreter. |
Concurrency Parse Option
For schemas like Struct, Array, or Union that contain multiple effectful schemas, the concurrency parse option controls how many parsing effects may run concurrently.
type Concurrency = number | "unbounded" | undefinedHere’s a shorter version presented in a table:
| Value | Description |
|---|---|
number |
Limits the maximum number of concurrent tasks. |
"unbounded" |
All tasks run concurrently with no limit. |
undefined |
At most one task runs at a time (the default). |
Example (Sequential Execution)
In this example, we define three tasks that simulate asynchronous operations with different durations. Since no concurrency is specified, the tasks are executed sequentially, one after the other.
import { Schema, SchemaGetter } from "effect"import type { Duration } from "effect"import { Effect } from "effect"
// Simulates an async taskconst item = (id: number, duration: Duration.Input) => Schema.String.pipe( Schema.decode({ decode: SchemaGetter.checkEffect(() => Effect.gen(function* () { yield* Effect.sleep(duration) console.log(`Task ${id} done`) return true }), ), encode: SchemaGetter.passthrough(), }), )
const Sequential = Schema.Tuple([ item(1, "30 millis"), item(2, "10 millis"), item(3, "20 millis"),])
Effect.runPromise(Schema.decodeEffect(Sequential)(["a", "b", "c"]))/*Output:Task 1 doneTask 2 doneTask 3 done*/Example (Concurrent Execution)
By passing { concurrency: "unbounded" } to the interpreter, the tasks can run concurrently instead of waiting for one another.
import { Schema, SchemaGetter } from "effect"import type { Duration } from "effect"import { Effect } from "effect"
// Simulates an async taskconst item = (id: number, duration: Duration.Input) => Schema.String.pipe( Schema.decode({ decode: SchemaGetter.checkEffect(() => Effect.gen(function* () { yield* Effect.sleep(duration) console.log(`Task ${id} done`) return true }), ), encode: SchemaGetter.passthrough(), }), )
const Concurrent = Schema.Tuple([ item(1, "30 millis"), item(2, "10 millis"), item(3, "20 millis"),])
Effect.runPromise( Schema.decodeEffect(Concurrent, { concurrency: "unbounded" })([ "a", "b", "c", ]),)/*Output:Task 2 doneTask 3 doneTask 1 done*/Handling Decoding Errors with Fallbacks
Schema.catchDecoding lets you recover from a decoding issue with fallback logic.
type DecodingFallback<T> = ( issue: SchemaIssue.Issue,) => Effect.Effect<Option.Option<T>, SchemaIssue.Issue>This annotation enables you to specify fallback behavior when decoding fails, making it possible to recover gracefully from errors.
Example (Basic Fallback)
In this basic example, when decoding fails (e.g., the input is null), the fallback value is returned instead of an error.
import { Schema } from "effect"import { Effect } from "effect"
// Schema with a fallback valueconst schema = Schema.String.pipe( Schema.catchDecoding(() => Effect.succeedSome("<fallback>")),)
console.log(Schema.decodeUnknownSync(schema)("valid input"))// Output: valid input
console.log(Schema.decodeUnknownSync(schema)(null))// Output: <fallback>Example (Advanced Fallback with Logging)
In this advanced example, when a decoding error occurs, the schema logs the issue and then returns a fallback value. This demonstrates how you can incorporate logging and other side effects during error handling.
import { Schema } from "effect"import { Effect } from "effect"
// Schema with logging and fallbackconst schemaWithLog = Schema.String.pipe( Schema.catchDecoding((issue) => Effect.gen(function* () { // Log the error issue yield* Effect.log(issue._tag) // Simulate a delay yield* Effect.sleep(10) // Return a fallback value return yield* Effect.succeedSome("<fallback>") }), ),)
// Run the effectful fallback logicEffect.runPromise(Schema.decodeUnknownEffect(schemaWithLog)(null)).then( console.log,)/*Output:timestamp=... level=INFO fiber=#0 message=InvalidType<fallback>*/Custom Annotations
In addition to built-in annotations, you can define custom annotations to meet specific requirements. For instance, here’s how to create a deprecated annotation:
Example (Defining a Custom Annotation)
import { Schema } from "effect"
// Define a unique identifier for your custom annotationconst DeprecatedId = Symbol.for( "some/unique/identifier/for/your/custom/annotation",)
// Apply the custom annotation to the schemaconst MyString = Schema.String.annotate({ [DeprecatedId]: true })To make your new custom annotation type-safe, you can use a module augmentation. In the next example, we want our custom annotation to be a boolean.
Example (Adding Type Safety to Custom Annotations)
import { Schema } from "effect"
const DeprecatedId = Symbol.for( "some/unique/identifier/for/your/custom/annotation",)
// Module augmentationdeclare module "effect/Schema" { namespace Annotations { interface Annotations { [DeprecatedId]?: boolean } }}
const MyString = Schema.String.annotate({ // @errors: 2418 [DeprecatedId]: "bad value",})You can retrieve custom annotations using the Schema.resolveAnnotations helper function.
Example (Retrieving a Custom Annotation)
import { Schema } from "effect"
13 collapsed lines
const DeprecatedId = Symbol.for( "some/unique/identifier/for/your/custom/annotation",)
declare module "effect/Schema" { namespace Annotations { interface Annotations { [DeprecatedId]?: boolean } }}
const MyString = Schema.String.annotate({ [DeprecatedId]: true })
// Helper function to check if a schema is marked as deprecatedconst isDeprecated = (schema: Schema.Top): boolean => Schema.resolveAnnotations(schema)?.[DeprecatedId] ?? false
console.log(isDeprecated(Schema.String))// Output: false
console.log(isDeprecated(MyString))// Output: true