# Annotations

Annotations are metadata attached to a schema. They never change how a value is
decoded, encoded, or validated — they exist so that *tooling* can do a better
job. The default error formatter uses them to print friendlier messages, the
JSON Schema generator copies them into `title` / `description` / `examples`, and
derivations (Arbitrary, Equivalence, custom hooks) read them to control how they
build their result.

```ts
import { Schema } from "effect"

// A self-documenting schema. None of these keys affect parsing — they describe
// the value so error messages, JSON Schema, and editors can show useful text.
const Age = Schema.Number.pipe(
  Schema.annotate({
    identifier: "Age",
    title: "Age",
    description: "A non-negative integer representing age in years",
    examples: [0, 18, 42]
  })
)

// The annotations show up in the generated JSON Schema document:
console.log(Schema.toJsonSchemaDocument(Age).schema)
// {
//   type: "number",
//   title: "Age",
//   description: "A non-negative integer representing age in years",
//   examples: [0, 18, 42]
// }
```

Annotations come in three flavors depending on *what* you are describing:

- **Schema annotations** — describe a value type. Attach with `.annotate(...)`
  or the `Schema.annotate(...)` combinator.
- **Key annotations** — describe a *field's position* inside a `Struct` or
  `Tuple` (not its value). Attach with `.annotateKey(...)` or
  `Schema.annotateKey(...)`.
- **Encoded-side annotations** — describe the *wire* representation rather than
  the decoded value. Attach with `Schema.annotateEncoded(...)`.

## Why annotate

### Better error messages

The default formatter (see [Error Formatting](https://effect.plants.sh/schema/error-formatting/)) looks
at annotations when it builds a message. For a *type* mismatch it uses
`identifier` (then `title`) as the expected label; for a *filter* failure it
uses `message`, then `expected`.

```ts
import { Schema } from "effect"

const UserId = Schema.String.annotate({ identifier: "UserId" })

console.log(String(Schema.decodeUnknownExit(UserId)(123)))
// Failure(Cause([Fail(SchemaError(Expected UserId, got 123))]))
```

```ts
import { Schema } from "effect"

// `message` replaces the whole filter-failure message; `expected` only renames
// the "Expected ..." label. Put these on the check, not the base schema.
const Password = Schema.String.check(
  Schema.isMinLength(8, { message: "Password must be at least 8 characters" })
)

console.log(String(Schema.decodeUnknownExit(Password)("short")))
// Failure(Cause([Fail(SchemaError(Password must be at least 8 characters))]))
```
**identifier does not name a failed filter:** `identifier` (and `title`) name the *type*, so they only appear when the base
  type is wrong. If the type matches but a filter fails, annotate the
  filter/refinement with `expected` or `message` instead.

### Documentation generators

`Schema.toJsonSchemaDocument` copies the standard documentation annotations
(`title`, `description`, `default`, `examples`, `readOnly`, `writeOnly`,
`format`, `contentEncoding`, `contentMediaType`) straight into the output. See
[JSON Schema](https://effect.plants.sh/schema/json-schema/) for the full mapping.

```ts
import { Schema } from "effect"

const Port = Schema.Number.pipe(
  Schema.annotate({
    title: "Port",
    description: "TCP port number",
    default: 8080,
    examples: [80, 443, 8080]
  })
)

console.log(Schema.toJsonSchemaDocument(Port).schema)
// {
//   type: "number",
//   title: "Port",
//   description: "TCP port number",
//   default: 8080,
//   examples: [80, 443, 8080]
// }
```

### Identification and reuse

An `identifier` gives a schema a stable name. JSON Schema generation uses it to
emit `$ref` definitions instead of inlining the same shape repeatedly, and the
formatter uses it as the expected label.

```ts
import { Schema } from "effect"

const Email = Schema.String.annotate({ identifier: "Email" })

const Contact = Schema.Struct({
  primary: Email,
  backup: Email
})

const doc = Schema.toJsonSchemaDocument(Contact)
console.log(doc.schema)
// { type: "object", properties: { primary: { $ref: "#/$defs/Email" }, ... }, ... }
console.log(Object.keys(doc.definitions))
// ["Email"]
```

## The annotation shape

Every annotation object is, at its widest, a record of `string` keys to
`unknown` values (`Annotations.Annotations`). On top of that, the typed
interfaces in the `Schema.Annotations` namespace describe the *well-known* keys
that tooling understands. The interface you get depends on the combinator:

| Combinator | Annotation interface | Notable keys beyond the common set |
| --- | --- | --- |
| `annotate` / `.annotate` | `Annotations.Bottom<T, ...>` | `message`, `messageUnexpectedKey`, `identifier`, `parseOptions`, `meta`, `brands`, `toArbitrary` |
| `annotateKey` / `.annotateKey` | `Annotations.Key<T>` | `messageMissingKey` |
| `annotateEncoded` | `Annotations.Bottom<Encoded, []>` | same as `annotate`, applied to the encoded view |
| check builders / `makeFilter` | `Annotations.Filter` | `message`, `expected`, `identifier`, `meta` |

The common documentation keys (`Annotations.Documentation<T>`, which extends
`Annotations.Augment`) are shared by all of them:

```ts
import { Schema } from "effect"

// Every key here is recognized by the typed annotation interface.
const schema = Schema.String.annotate({
  // From Augment (map directly onto JSON Schema / OpenAPI fields):
  title: "Slug",
  description: "URL-safe identifier",
  documentation: "See RFC 3986 for the allowed character set",
  readOnly: true,
  format: "uri-reference",
  contentEncoding: "utf-8",
  contentMediaType: "text/plain",
  // From Documentation<T> (type-parametric):
  default: "untitled",
  examples: ["my-post", "hello-world"],
  // From Bottom<T, ...>:
  identifier: "Slug",
  message: "Expected a URL-safe slug"
})
```

Annotations are merged, not replaced — calling `.annotate` twice combines the
two objects (later keys win). Set a key to `undefined` to remove it.

```ts
import { Schema } from "effect"

const base = Schema.String.annotate({ title: "A", description: "first" })

const merged = base.annotate({ description: "second" })
console.log(Schema.resolveAnnotations(merged)?.title) // "A"
console.log(Schema.resolveAnnotations(merged)?.description) // "second"

const cleared = merged.annotate({ description: undefined })
console.log(Schema.resolveAnnotations(cleared)?.description) // undefined
```

### Custom annotations

Any extra key is preserved on the AST, so derivations and your own tooling can
read it. You can extend the `Annotations.Annotations` interface via module
augmentation to make a custom key type-safe.

```ts
import { Schema } from "effect"

// Make a `version` annotation known to the compiler everywhere.
declare module "effect/Schema" {
  namespace Annotations {
    interface Annotations {
      readonly version?:
        | readonly [major: number, minor: number, patch: number]
        | undefined
    }
  }
}

const schema = Schema.String.annotate({ version: [1, 2, 0] })
const version = Schema.resolveAnnotations(schema)?.version
console.log(version?.[1]) // => 2 (the minor component)
```
**Custom keys and JSON Schema:** Custom annotation keys are *not* emitted into JSON Schema by default. Pass
  `includeAnnotationKey` to `toJsonSchemaDocument` to whitelist the ones you
  want (e.g. `key.startsWith("x-")`). See [JSON Schema](https://effect.plants.sh/schema/json-schema/).

## Annotations on checks

Checks (filters) carry their own `Annotations.Filter`, which is where the
formatter looks for failure text. The default formatter checks `message` first,
then `expected`, then falls back to `<filter>`. Every built-in check builder
accepts an optional annotations argument, so you can override its message inline.

```ts
import { Schema } from "effect"

// `expected` renames the "Expected ..." label; `message` replaces the whole line.
// `makeFilter`'s second argument is the filter's annotations.
const Even = Schema.Number.check(
  Schema.makeFilter(
    (n) => n % 2 === 0,
    { expected: "an even number" }
  )
)

console.log(String(Schema.decodeUnknownExit(Even)(3)))
// Failure(Cause([Fail(SchemaError(Expected an even number, got 3))]))
```

```ts
import { Schema } from "effect"

// Built-in checks take annotations as a trailing argument.
const Username = Schema.String.check(
  Schema.isMinLength(3, { message: "Username is too short" }),
  Schema.isMaxLength(20, { message: "Username is too long" })
)

console.log(String(Schema.decodeUnknownExit(Username)("ab")))
// Failure(Cause([Fail(SchemaError(Username is too short))]))
```

`Schema.makeFilter` (for `.check`) and the pipeable `Schema.refine` (a type
guard combinator) both take an `Annotations.Filter` argument, so custom checks
participate in the same scheme. See [Filters](https://effect.plants.sh/schema/filters/) for the full
check catalog.

## How derivations consume annotations

Annotations are the extension point for every derivation that ships with Schema:

- **JSON Schema** — `toJsonSchemaDocument` copies the documentation keys and
  uses `identifier` for `$ref` definition names; `includeAnnotationKey` opts in
  custom keys. See [JSON Schema](https://effect.plants.sh/schema/json-schema/).
- **Error formatting** — the default formatter consumes `message`, `expected`,
  `identifier`, `title`, and `messageMissingKey` / `messageUnexpectedKey`. See
  [Error Formatting](https://effect.plants.sh/schema/error-formatting/).
- **Arbitrary** — the `toArbitrary` hook (on `Annotations.Bottom` /
  `Annotations.Declaration`) and the `toArbitraryConstraint` hook on filters let
  a schema control how property-test data is generated.
- **Custom derivations** — anything you read via `resolveAnnotations` /
  `SchemaAST.resolve` can drive your own tooling. Module-augment
  `Annotations.Annotations` to type your keys.

```ts
import { Schema } from "effect"

// A custom annotation read by your own code generator.
const Field = Schema.String.annotate({ "x-db-column": "user_name" })
console.log(Schema.resolveAnnotations(Field)?.["x-db-column"])
// => "user_name"
```

---

## Reference

### annotate

Adds metadata to the **decoded** (Type) side of a schema. Pipeable counterpart
of the `.annotate` method. Accepts `Annotations.Bottom<T, ...>`.

```ts
import { Schema } from "effect"

const Name = Schema.String.pipe(
  Schema.annotate({ title: "Name", description: "The user's display name" })
)

console.log(Schema.resolveAnnotations(Name)?.title) // => "Name"
```

### .annotate (method)

The method form of `annotate`, available on every schema. Returns a rebuilt
schema of the same type with the annotations merged in.

```ts
import { Schema } from "effect"

const Name = Schema.String.annotate({ title: "Name" })
console.log(Schema.resolveAnnotations(Name)?.title) // => "Name"
```

### annotateEncoded

Adds metadata to the **encoded** (wire) side of a schema. Internally the schema
is flipped so `Encoded` becomes `Type`, annotated, then flipped back. Use this
to document what the *serialized* form looks like, independently of the decoded
value.

```ts
import { Schema } from "effect"

// Decoded side is a number; the encoded side is a string. Annotate each side
// separately so each view of the schema documents itself.
const schema = Schema.NumberFromString.pipe(
  Schema.annotate({ title: "Count (number)" }),
  Schema.annotateEncoded({ title: "Count (string on the wire)" })
)

console.log(Schema.resolveAnnotations(schema)?.title)
// => "Count (number)"
console.log(Schema.toEncoded(schema).ast.annotations?.title)
// => "Count (string on the wire)"
```
**annotateEncoded is new in v4:** v3 only let you annotate a single side. `annotateEncoded` lets the encoded
  representation carry its own `title` / `description` / `examples`, which flows
  into the JSON Schema of the wire format and into encode-direction errors.

### annotateKey

Adds **key-level** metadata to a field — describing its slot inside a `Struct`
or `Tuple` rather than its value type. Accepts `Annotations.Key<T>`, whose extra
key `messageMissingKey` overrides the error shown when the field is absent.

```ts
import { Schema } from "effect"

const Form = Schema.Struct({
  username: Schema.String.pipe(
    Schema.annotateKey({
      description: "The username used to log in",
      messageMissingKey: "Username is required"
    })
  )
})

console.log(String(Schema.decodeUnknownExit(Form)({})))
// Failure(Cause([Fail(SchemaError(Username is required
//   at ["username"]
// ))]))
```

### .annotateKey (method)

The method form of `annotateKey`, available on every schema. Useful inline
inside a `Struct` definition.

```ts
import { Schema } from "effect"

const field = Schema.Number.annotateKey({ description: "1-based page index" })
console.log(Schema.resolveAnnotationsKey(field)?.description)
// => "1-based page index"
```

### resolveAnnotations

Reads the merged schema annotations at runtime, typed as
`Annotations.Bottom<T, ...>`. If the schema has checks, the annotations come
from the *last* check; otherwise from the base node. Returns `undefined` when
there are none.

```ts
import { Schema } from "effect"

const Age = Schema.Number.annotate({ title: "Age", identifier: "Age" })
console.log(Schema.resolveAnnotations(Age))
// => { title: "Age", identifier: "Age" }
```

### resolveAnnotationsKey

Reads the key-level annotations attached via `annotateKey` (stored on the AST's
`context`, not on the schema node itself). Returns `Annotations.Key<T>` or
`undefined`.

```ts
import { Schema } from "effect"

const field = Schema.String.annotateKey({ messageMissingKey: "required" })
console.log(Schema.resolveAnnotationsKey(field)?.messageMissingKey)
// => "required"
```

## Reading annotations from the AST

`Schema.resolveAnnotations` / `Schema.resolveAnnotationsKey` are the typed,
schema-level readers. When you work at the AST level (e.g. building a custom
derivation) the `SchemaAST` module exposes lower-level resolvers that take an
`AST` and apply the same "last check wins" lookup.

### SchemaAST.resolve

Returns the merged annotation record for an AST node, or `undefined`.

```ts
import { Schema, SchemaAST } from "effect"

const schema = Schema.String.annotate({ title: "Name" })
console.log(SchemaAST.resolve(schema.ast)?.title) // => "Name"
```

### SchemaAST.resolveAt

Returns a single annotation value by key, using the same lookup.

```ts
import { Schema, SchemaAST } from "effect"

const schema = Schema.String.annotate({ title: "Name" })
const getTitle = SchemaAST.resolveAt<string>("title")
console.log(getTitle(schema.ast)) // => "Name"
```

### SchemaAST.resolveIdentifier

Shorthand for the `identifier` annotation.

```ts
import { Schema, SchemaAST } from "effect"

const schema = Schema.String.annotate({ identifier: "UserId" })
console.log(SchemaAST.resolveIdentifier(schema.ast)) // => "UserId"
```

### SchemaAST.resolveTitle

Shorthand for the `title` annotation.

```ts
import { Schema, SchemaAST } from "effect"

const schema = Schema.String.annotate({ title: "Name" })
console.log(SchemaAST.resolveTitle(schema.ast)) // => "Name"
```

### SchemaAST.resolveDescription

Shorthand for the `description` annotation.

```ts
import { Schema, SchemaAST } from "effect"

const schema = Schema.String.annotate({ description: "A label" })
console.log(SchemaAST.resolveDescription(schema.ast)) // => "A label"
```