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 with three fields:

  • dialect — the JSON Schema dialect ("draft-2020-12").
  • schema — the generated JSON Schema for your type.
  • definitions — a pool of reusable sub-schemas referenced via $ref (for example, shared or recursive shapes).

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 an options 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.
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.