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// }The document shape
Section titled “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 oftoJsonSchemaDocument.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
Section titled “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.
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
Section titled “Options”toJsonSchemaDocument accepts a Schema.ToJsonSchemaOptions object to tune the
output:
additionalProperties—false(default) forbids extra keys,trueallows them, or pass aJsonSchemato constrain them.generateDescriptions— synthesizedescriptiontext for checks from theirexpectedannotation 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" }Targeting other dialects
Section titled “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 $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.
The JsonSchema module
Section titled “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
$refpointers 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.
JsonSchema (model)
Section titled “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.
import { JsonSchema } from "effect"
const node: JsonSchema.JsonSchema = { type: "object", properties: { name: { type: "string" } }, required: ["name"]}// => any JSON Schema keyword is allowedDialect (model)
Section titled “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.
import { JsonSchema } from "effect"
const dialect: JsonSchema.Dialect = "draft-2020-12"// => "draft-07" | "draft-2020-12" | "openapi-3.1" | "openapi-3.0"Type (model)
Section titled “Type (model)”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"Definitions (model)
Section titled “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.
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)
Section titled “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).
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)
Section titled “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.
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)
Section titled “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.
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)
Section titled “META_SCHEMA_URI_DRAFT_2020_12 (constant)”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"Parsing external schemas (from*)
Section titled “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
Section titled “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.
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
Section titled “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.
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
Section titled “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.
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
Section titled “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.
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*)
Section titled “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
Section titled “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.
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
Section titled “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.
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
Section titled “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
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"))// => trueconsole.log(JsonSchema.VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test("User Profile"))// => false (contains a space)sanitizeOpenApiComponentsSchemasKey
Section titled “sanitizeOpenApiComponentsSchemasKey”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(""))// => "_"Resolving $ref pointers
Section titled “Resolving $ref pointers”resolve$ref
Section titled “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.
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))// => undefinedresolveTopLevel$ref
Section titled “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.
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
Section titled “See also”- 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.