Skip to content
Effect Days 2026 Get your ticket
Docs menu / Annotations

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 type
const 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:

AnnotationScopeDescription
identifierschemaStable name used by schema interpreters, including JSON Schema references and expected-value messages.
expectedschema or checkHuman-readable description used by the default error formatter.
titleschema or keyShort display title, also understood by JSON Schema tooling.
descriptionschema or keyLonger documentation for the represented value.
documentationschema or keyAdditional developer-facing documentation.
examplesschema or keyExample decoded values; this is metadata and does not validate them.
defaultschema or keyA documented default value; it does not change decoding or construction behavior.
messageschema or checkReplaces the default message for matching failures.
messageMissingKeykeyReplaces the error message when a required key is absent.
messageUnexpectedKeyschemaReplaces the message for excess keys when onExcessProperty is "error".
parseOptionsschemaOverrides parse options for that schema node.
toJsonSchemacheckDescribes a custom check to the JSON Schema interpreter.
toArbitraryschema or declarationCustomizes Arbitrary generation.
toFormatterdeclarationDefines Formatter behavior for a custom declaration.
toEquivalencedeclarationDefines Equivalence behavior for a custom declaration.
toCodecJsondeclarationDefines 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" | undefined

Here’s a shorter version presented in a table:

ValueDescription
numberLimits the maximum number of concurrent tasks.
"unbounded"All tasks run concurrently with no limit.
undefinedAt 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 task
const 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 done
Task 2 done
Task 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 task
const 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 done
Task 3 done
Task 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 value
const 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 fallback
const 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 logic
Effect.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 annotation
const DeprecatedId = Symbol.for(
"some/unique/identifier/for/your/custom/annotation",
)
// Apply the custom annotation to the schema
const 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 augmentation
declare 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 deprecated
const isDeprecated = (schema: Schema.Top): boolean =>
Schema.resolveAnnotations(schema)?.[DeprecatedId] ?? false
console.log(isDeprecated(Schema.String))
// Output: false
console.log(isDeprecated(MyString))
// Output: true