# JSON Schema

Because a [schema](https://effect.plants.sh/schema/) already describes your data completely, Effect can
derive a standard **JSON Schema** document from it. This is how the same schema
you use to decode and validate at runtime also drives external tooling: OpenAPI
documents, editor autocompletion, form generators, and contract validation in
other languages.

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

const User = Schema.Struct({
  name: Schema.String.check(Schema.isMinLength(1)),
  age: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
  email: Schema.optionalKey(Schema.String)
})

// Produces a JSON Schema document targeting draft 2020-12.
const document = Schema.toJsonSchemaDocument(User)

console.log(JSON.stringify(document.schema, null, 2))
// {
//   "type": "object",
//   "properties": {
//     "name": { "type": "string", "minLength": 1 },
//     "age": { "type": "number", "minimum": 0 },
//     "email": { "type": "string" }
//   },
//   "required": ["name", "age"],
//   "additionalProperties": false
// }
```

## The document shape

`Schema.toJsonSchemaDocument` returns a `JsonSchema.Document<"draft-2020-12">`
with three fields:

- `dialect` — the JSON Schema dialect, always `"draft-2020-12"` for the output of
  `toJsonSchemaDocument`.
- `schema` — the generated JSON Schema for your type (the root schema, *without*
  the definitions collection inlined).
- `definitions` — a pool of reusable sub-schemas referenced via `$ref` (for
  example, shared or recursive shapes). Stored separately so it can be relocated
  when converting between dialects.

Filters that have a JSON Schema equivalent are emitted automatically:
`isMinLength` becomes `minLength`, `isGreaterThanOrEqualTo` becomes `minimum`,
`isPattern` becomes `pattern`, and so on. An exact-optional field
(`Schema.optionalKey`) is simply omitted from the `required` array.

## Enriching the output with annotations

Annotations such as `title`, `description`, `examples`, and `default` flow
straight into the generated schema. Annotate your schema to produce
documentation-quality output.

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

const Product = Schema.Struct({
  sku: Schema.String.annotate({
    title: "SKU",
    description: "Stock keeping unit",
    examples: ["ABC-123"]
  }),
  price: Schema.Number.check(Schema.isGreaterThan(0)).annotate({
    description: "Price in USD"
  })
}).annotate({ title: "Product" })

const document = Schema.toJsonSchemaDocument(Product)

console.log(document.schema.title) // "Product"
```

## Options

`toJsonSchemaDocument` accepts a `Schema.ToJsonSchemaOptions` object to tune the
output:

- `additionalProperties` — `false` (default) forbids extra keys, `true` allows
  them, or pass a `JsonSchema` to constrain them.
- `generateDescriptions` — synthesize `description` text for checks from their
  `expected` annotation when you have not supplied one.
- `includeAnnotationKey` — a predicate selecting which non-standard annotation
  keys (vendor extensions, editor hints) to emit. The standard JSON Schema keys
  (`title`, `description`, `default`, `examples`, `readOnly`, `writeOnly`,
  `format`, `contentEncoding`, `contentMediaType`, `contentSchema`) are always
  included; the predicate is only consulted for *other* keys.

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

const schema = Schema.String.annotate({
  description: "A name",
  // A custom, non-standard annotation key.
  markdownDescription: "The **name** field"
})

const document = Schema.toJsonSchemaDocument(schema, {
  // Whitelist exactly the custom keys you want to surface.
  includeAnnotationKey: (key) =>
    key === "markdownDescription" || key.startsWith("x-")
})

console.log(document.schema)
// { type: "string", description: "A name", markdownDescription: "The **name** field" }
```
**Caution:** Prefer whitelisting specific annotation keys over a broad predicate like
`() => true`. A broad predicate can leak Effect-internal annotations that are
meant only for schema generation into your public JSON Schema.

## Targeting other dialects

The generated document targets draft 2020-12. To emit an older draft, convert it
with the helpers in the `JsonSchema` module — for example
`JsonSchema.toDocumentDraft07` rewrites the document (and its `$ref`s) to
draft-07.

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

const User = Schema.Struct({ name: Schema.String })

const draft202012 = Schema.toJsonSchemaDocument(User)
const draft07 = JsonSchema.toDocumentDraft07(draft202012)

console.log(draft07.dialect) // "draft-07"
```

For request/response contracts you usually do not call this directly — the
[HTTP API](https://effect.plants.sh/http-api/openapi/) layer generates OpenAPI documents from your
schemas for you. `toJsonSchemaDocument` is the building block underneath.

## The `JsonSchema` module

`Schema.toJsonSchemaDocument` is the most common entry point, but the standalone
`JsonSchema` module (imported from `"effect"`) is a complete toolkit for working
with JSON Schema documents independently of `Schema`. It lets you:

- **parse** raw JSON Schema objects of any supported dialect into a canonical
  internal representation (the `from*` functions),
- **convert** that representation to a different output dialect (the `to*`
  functions),
- **resolve** `$ref` pointers against a definitions pool.

The mental model is that every dialect is normalized into a single internal form
— `Document<"draft-2020-12">` — and conversions go *into* and *out of* that form.
**Note:** Conversion is best-effort: unrecognized or unsupported JSON Schema keywords are
silently dropped, and dialect-specific syntax (Draft-07 tuples, OpenAPI 3.0
`nullable`/`example`) is rewritten. Keep this in mind when round-tripping
hand-written schemas.

### `JsonSchema` (model)

An open record type (`{ [x: string]: unknown }`) representing a single JSON
Schema node, of any dialect. Most functions in the module accept or return this
type.

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

const node: JsonSchema.JsonSchema = {
  type: "object",
  properties: { name: { type: "string" } },
  required: ["name"]
}
// => any JSON Schema keyword is allowed
```

### `Dialect` (model)

The union of supported dialects: `"draft-07" | "draft-2020-12" | "openapi-3.1" |
"openapi-3.0"`. Used as the `dialect` tag on `Document` and `MultiDocument`.

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

const dialect: JsonSchema.Dialect = "draft-2020-12"
// => "draft-07" | "draft-2020-12" | "openapi-3.1" | "openapi-3.0"
```

### `Type` (model)

The union of JSON Schema primitive type names: `"string" | "number" | "boolean" |
"array" | "object" | "null" | "integer"`. Useful when typing a `type` keyword.

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

const t: JsonSchema.Type = "integer"
// => one of "string" | "number" | "boolean" | "array" | "object" | "null" | "integer"
```

### `Definitions` (model)

A `Record<string, JsonSchema>` keyed by definition name. Dialect-neutral: the
same map is emitted as `$defs`, `definitions`, or `components.schemas` depending
on the target format.

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

const definitions: JsonSchema.Definitions = {
  User: { type: "object", properties: { name: { type: "string" } } }
}
// => { User: { type: "object", properties: { name: { type: "string" } } } }
```

### `Document<D>` (model)

A container holding a single root `schema`, its companion `definitions`, and the
target `dialect`. The root `schema` does **not** inline its definitions — they
live separately in `definitions` and are referenced via `#/$defs/<name>`
(draft-2020-12), `#/definitions/<name>` (draft-07), or
`#/components/schemas/<name>` (OpenAPI).

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

const doc: JsonSchema.Document<"draft-2020-12"> = {
  dialect: "draft-2020-12",
  schema: { $ref: "#/$defs/User" },
  definitions: {
    User: { type: "object", properties: { name: { type: "string" } } }
  }
}
// => { dialect, schema, definitions }
```

### `MultiDocument<D>` (model)

Like `Document`, but carries multiple root `schemas` (a non-empty tuple) that
share a single `definitions` pool. Useful when generating several related
schemas — for example a request body and a response body — that reference the
same definitions.

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

const multi: JsonSchema.MultiDocument<"draft-2020-12"> = {
  dialect: "draft-2020-12",
  schemas: [{ $ref: "#/$defs/User" }, { $ref: "#/$defs/Order" }],
  definitions: {
    User: { type: "object" },
    Order: { type: "object" }
  }
}
// => { dialect, schemas: [root1, root2], definitions }
```

### `META_SCHEMA_URI_DRAFT_07` (constant)

The literal `$schema` meta-schema URI for JSON Schema Draft-07. Use it when
populating the root `$schema` field of a draft-07 document.

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

console.log(JsonSchema.META_SCHEMA_URI_DRAFT_07)
// => "http://json-schema.org/draft-07/schema"
```

### `META_SCHEMA_URI_DRAFT_2020_12` (constant)

The literal `$schema` meta-schema URI for JSON Schema Draft 2020-12.

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

console.log(JsonSchema.META_SCHEMA_URI_DRAFT_2020_12)
// => "https://json-schema.org/draft/2020-12/schema"
```

## Parsing external schemas (`from*`)

All four `from*` functions take a raw `JsonSchema` and normalize it **into** a
`Document<"draft-2020-12">`, regardless of the input dialect. This is how you
bring an externally-authored schema into the canonical internal form before
inspecting or converting it.

### `fromSchemaDraft07`

Parses a Draft-07 schema into `Document<"draft-2020-12">`. Converts Draft-07
tuple syntax (`items` as an array plus `additionalItems`) into 2020-12 form
(`prefixItems` plus `items`), rewrites `#/definitions/...` refs to `#/$defs/...`,
and lifts root-level `definitions` into the document's `definitions` field.
Unsupported keywords like `if`/`then`/`else` and `$id` are dropped.

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

const raw: JsonSchema.JsonSchema = {
  type: "object",
  properties: {
    tags: { type: "array", items: { type: "string" } }
  },
  definitions: { Trimmed: { type: "string", minLength: 1 } }
}

const doc = JsonSchema.fromSchemaDraft07(raw)
console.log(doc.dialect) // => "draft-2020-12"
console.log(doc.schema.properties)
// => { tags: { type: "array", items: { type: "string" } } }
console.log(doc.definitions)
// => { Trimmed: { type: "string", minLength: 1 } }
```

### `fromSchemaDraft2020_12`

Parses a Draft-2020-12 schema into `Document<"draft-2020-12">`. It only separates
the root `$defs` out into the `definitions` field; no keyword rewriting is
performed.

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

const raw: JsonSchema.JsonSchema = {
  type: "number",
  minimum: 0,
  $defs: { PositiveInt: { type: "integer", minimum: 1 } }
}

const doc = JsonSchema.fromSchemaDraft2020_12(raw)
console.log(doc.schema) // => { type: "number", minimum: 0 }
console.log(doc.definitions) // => { PositiveInt: { type: "integer", minimum: 1 } }
```

### `fromSchemaOpenApi3_1`

Parses an OpenAPI 3.1 schema into `Document<"draft-2020-12">`. It rewrites
`#/components/schemas/...` refs to `#/$defs/...`, then delegates to
`fromSchemaDraft2020_12`.

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

const raw: JsonSchema.JsonSchema = {
  type: "object",
  properties: { user: { $ref: "#/components/schemas/User" } }
}

const doc = JsonSchema.fromSchemaOpenApi3_1(raw)
console.log(doc.schema.properties)
// => { user: { $ref: "#/$defs/User" } }
```

### `fromSchemaOpenApi3_0`

Parses an OpenAPI 3.0 schema into `Document<"draft-2020-12">`. It handles
OpenAPI 3.0 extensions — `nullable: true` is expanded into a `type` array (or
`anyOf` when no `type` is present), singular `example` becomes an `examples`
array, and boolean `exclusiveMinimum`/`exclusiveMaximum` are normalized — by
first normalizing to Draft-07, then delegating to `fromSchemaDraft07`.

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

const raw: JsonSchema.JsonSchema = {
  type: "string",
  nullable: true,
  example: "hello"
}

const doc = JsonSchema.fromSchemaOpenApi3_0(raw)
console.log(doc.schema.type) // => ["string", "null"]
console.log(doc.schema.examples) // => ["hello"]
```

## Converting to other targets (`to*`)

Once you hold a canonical `Document<"draft-2020-12">` (from `Schema` or from a
`from*` parse), the `to*` functions emit a different output dialect, rewriting
`$ref` pointers and dialect-specific syntax along the way.

### `toDocumentDraft07`

Converts a `Document<"draft-2020-12">` to a `Document<"draft-07">`. It rewrites
`#/$defs/...` refs to `#/definitions/...`, converts 2020-12 tuple syntax
(`prefixItems` plus `items`) back to Draft-07 form (`items` array plus
`additionalItems`), and converts the root schema and every definition.

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

const doc = JsonSchema.fromSchemaDraft2020_12({
  type: "array",
  prefixItems: [{ type: "string" }, { type: "number" }],
  items: { type: "boolean" }
})

const draft07 = JsonSchema.toDocumentDraft07(doc)
console.log(draft07.dialect) // => "draft-07"
console.log(draft07.schema.items)
// => [{ type: "string" }, { type: "number" }]
console.log(draft07.schema.additionalItems) // => { type: "boolean" }
```

### `toMultiDocumentOpenApi3_1`

Converts a `MultiDocument<"draft-2020-12">` to a `MultiDocument<"openapi-3.1">`.
It rewrites `#/$defs/...` refs to `#/components/schemas/...`, sanitizes definition
keys to the OpenAPI component-key pattern (replacing invalid characters with
`_`), and updates every `$ref` pointer to use the sanitized keys.

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

const multi: JsonSchema.MultiDocument<"draft-2020-12"> = {
  dialect: "draft-2020-12",
  schemas: [{ $ref: "#/$defs/User" }],
  definitions: {
    User: { type: "object", properties: { name: { type: "string" } } }
  }
}

const openapi = JsonSchema.toMultiDocumentOpenApi3_1(multi)
console.log(openapi.dialect) // => "openapi-3.1"
console.log(openapi.schemas[0]) // => { $ref: "#/components/schemas/User" }
```

## OpenAPI component-key sanitization

OpenAPI restricts `components.schemas` keys to a specific character set. These
helpers express and apply that rule; `toMultiDocumentOpenApi3_1` uses them
internally. They are exported but marked internal, so treat them as a low-level
escape hatch rather than stable API.

### `VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP`

The regular expression a valid OpenAPI component-schema key must match:
`/^[a-zA-Z0-9.\-_]+$/`.

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

console.log(JsonSchema.VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test("User.v1"))
// => true
console.log(JsonSchema.VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test("User Profile"))
// => false (contains a space)
```

### `sanitizeOpenApiComponentsSchemasKey`

Returns a sanitized version of a key, replacing every character outside the valid
set with `_` (and returning `"_"` for an empty string).

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

console.log(JsonSchema.sanitizeOpenApiComponentsSchemasKey("User Profile"))
// => "User_Profile"
console.log(JsonSchema.sanitizeOpenApiComponentsSchemasKey("OrderLine#1"))
// => "OrderLine_1"
console.log(JsonSchema.sanitizeOpenApiComponentsSchemasKey(""))
// => "_"
```

## Resolving `$ref` pointers

### `resolve$ref`

Looks up the **last** path segment of a `$ref` string in a `Definitions` map and
returns the schema it points to, or `undefined` if not found. It resolves the
final segment only (for example `"User"` from `"#/$defs/User"`) — it does not
follow arbitrary JSON Pointer paths.

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

const definitions: JsonSchema.Definitions = {
  User: { type: "object", properties: { name: { type: "string" } } }
}

console.log(JsonSchema.resolve$ref("#/$defs/User", definitions))
// => { type: "object", properties: { name: { type: "string" } } }

console.log(JsonSchema.resolve$ref("#/$defs/Unknown", definitions))
// => undefined
```

### `resolveTopLevel$ref`

If a document's root `schema` is itself a single top-level `$ref`, returns a
shallow copy of the document with that `$ref` inlined (resolved against its own
`definitions`). If the root is not a `$ref`, or it cannot be resolved, the
original document is returned unchanged.

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

const doc: JsonSchema.Document<"draft-2020-12"> = {
  dialect: "draft-2020-12",
  schema: { $ref: "#/$defs/User" },
  definitions: {
    User: { type: "object", properties: { name: { type: "string" } } }
  }
}

const resolved = JsonSchema.resolveTopLevel$ref(doc)
console.log(resolved.schema)
// => { type: "object", properties: { name: { type: "string" } } }
```

## See also

- [Building schemas](https://effect.plants.sh/schema/) — define the schemas you generate JSON Schema from.
- [OpenAPI generation](https://effect.plants.sh/http-api/openapi/) — generate complete OpenAPI documents
  from an HTTP API, built on top of `toJsonSchemaDocument`.