Skip to content

JSON Schema

Because a 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.

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
// }

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.

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

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"

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

  • additionalPropertiesfalse (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.
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" }

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 $refs) to draft-07.

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 layer generates OpenAPI documents from your schemas for you. toJsonSchemaDocument is the building block underneath.

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.

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.

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

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.

import { JsonSchema } from "effect"
const dialect: JsonSchema.Dialect = "draft-2020-12"
// => "draft-07" | "draft-2020-12" | "openapi-3.1" | "openapi-3.0"

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

import { JsonSchema } from "effect"
const t: JsonSchema.Type = "integer"
// => one of "string" | "number" | "boolean" | "array" | "object" | "null" | "integer"

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.

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

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).

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 }

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.

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 }

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

import { JsonSchema } from "effect"
console.log(JsonSchema.META_SCHEMA_URI_DRAFT_07)
// => "http://json-schema.org/draft-07/schema"

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

import { JsonSchema } from "effect"
console.log(JsonSchema.META_SCHEMA_URI_DRAFT_2020_12)
// => "https://json-schema.org/draft/2020-12/schema"

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.

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.

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 } }

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.

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 } }

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

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" } }

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.

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"]

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.

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.

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" }

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.

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 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

Section titled “VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP”

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

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)

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

import { JsonSchema } from "effect"
console.log(JsonSchema.sanitizeOpenApiComponentsSchemasKey("User Profile"))
// => "User_Profile"
console.log(JsonSchema.sanitizeOpenApiComponentsSchemasKey("OrderLine#1"))
// => "OrderLine_1"
console.log(JsonSchema.sanitizeOpenApiComponentsSchemasKey(""))
// => "_"

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.

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

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.

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" } } }
  • Building schemas — define the schemas you generate JSON Schema from.
  • OpenAPI generation — generate complete OpenAPI documents from an HTTP API, built on top of toJsonSchemaDocument.