Hyperlinkv0.8.0-beta.28

SchemaRepresentation

SchemaRepresentation.toSchemafunctioneffect/SchemaRepresentation.ts:1866
<S extends Schema.Top = Schema.Top>(
  document: Document,
  options?: { readonly reviver?: Reviver<Schema.Top> | undefined }
): S

Creates a runtime Schema from a Document.

When to use

Use when you have a serialized or computed schema representation document and need a runtime Schema for decoding/encoding.

Details

Pass options.reviver, such as toSchemaDefaultReviver, to handle Declaration nodes for types like Date and Option. Without a reviver, declarations fall back to their encodedSchema. Circular references are handled via lazy Schema.suspend.

Gotchas

This throws if a $ref is not found in document.references.

Example (Reconstructing a Schema)

import { Schema, SchemaRepresentation } from "effect"

const doc = SchemaRepresentation.fromAST(
  Schema.Struct({ name: Schema.String }).ast
)

const schema = SchemaRepresentation.toSchema(doc)
console.log(JSON.stringify(Schema.toJsonSchemaDocument(schema), null, 2))
export function toSchema<S extends Schema.Top = Schema.Top>(document: Document, options?: {
  readonly reviver?: Reviver<Schema.Top> | undefined
}): S {
  type Slot = {
    // 0 = not started, 1 = building, 2 = done
    state: 0 | 1 | 2
    value: Schema.Top | undefined
    ref: Schema.Top
  }

  const slots = new Map<string, Slot>()

  return recur(document.representation) as S

  function recur(r: Representation): Schema.Top {
    let out = on(r)
    if ("annotations" in r && r.annotations) out = out.annotate(r.annotations)
    out = toSchemaChecks(out, r)
    return out
  }

  function getSlot(identifier: string): Slot {
    const existing = slots.get(identifier)
    if (existing) return existing

    // Create the slot *before* resolving, so self-references can see it.
    const slot: Slot = {
      state: 0,
      value: undefined,
      ref: Schema.suspend(() => {
        if (slot.value === undefined) {
          return Schema.Unknown
        }
        return slot.value
      })
    }
    slots.set(identifier, slot)
    return slot
  }

  function resolveReference($ref: string): Schema.Top {
    const definition = document.references[$ref]
    if (definition === undefined) {
      throw new Error(`Reference ${$ref} not found`)
    }

    const slot = getSlot($ref)

    if (slot.state === 2) {
      // Already built: return the built schema directly
      return slot.value!
    }

    if (slot.state === 1) {
      // Circular: we're currently building this identifier.
      return slot.ref
    }

    // First time: build it.
    slot.state = 1
    try {
      slot.value = recur(definition)
      slot.state = 2
      return slot.value
    } catch (e) {
      // Leave the slot in a safe state so future thunks don't silently succeed.
      slot.state = 0
      slot.value = undefined
      throw e
    }
  }

  function on(r: Representation): Schema.Top {
    switch (r._tag) {
      case "Declaration":
        return options?.reviver?.(r, recur) ?? recur(r.encodedSchema)
      case "Reference":
        return resolveReference(r.$ref)
      case "Suspend":
        return recur(r.thunk)
      case "Null":
        return Schema.Null
      case "Undefined":
        return Schema.Undefined
      case "Void":
        return Schema.Void
      case "Never":
        return Schema.Never
      case "Unknown":
        return Schema.Unknown
      case "Any":
        return Schema.Any
      case "String": {
        const contentMediaType = r.contentMediaType
        const contentSchema = r.contentSchema
        if (contentMediaType === "application/json" && contentSchema !== undefined) {
          return Schema.fromJsonString(recur(contentSchema))
        }
        return Schema.String
      }
      case "Number":
        return Schema.Number
      case "Boolean":
        return Schema.Boolean
      case "BigInt":
        return Schema.BigInt
      case "Symbol":
        return Schema.Symbol
      case "Literal":
        return Schema.Literal(r.literal)
      case "UniqueSymbol":
        return Schema.UniqueSymbol(r.symbol)
      case "ObjectKeyword":
        return Schema.ObjectKeyword
      case "Enum":
        return Schema.Enum(Object.fromEntries(r.enums))
      case "TemplateLiteral": {
        const parts = r.parts.map(recur) as Schema.TemplateLiteral.Parts
        return Schema.TemplateLiteral(parts)
      }
      case "Arrays": {
        const elements = r.elements.map((e) => {
          const s = recur(e.type)
          return e.isOptional ? Schema.optionalKey(s) : s
        })
        const rest = r.rest.map(recur)
        if (Arr.isArrayNonEmpty(rest)) {
          if (r.elements.length === 0 && r.rest.length === 1) {
            return Schema.Array(rest[0])
          }
          return Schema.TupleWithRest(Schema.Tuple(elements), rest)
        }
        return Schema.Tuple(elements)
      }
      case "Objects": {
        const fields: Record<PropertyKey, Schema.Top> = {}

        for (const ps of r.propertySignatures) {
          const s = recur(ps.type)
          const withOptional = ps.isOptional ? Schema.optionalKey(s) : s
          fields[ps.name] = ps.isMutable ? Schema.mutableKey(withOptional) : withOptional
        }

        const indexSignatures = r.indexSignatures.map((is) =>
          Schema.Record(recur(is.parameter) as Schema.Record.Key, recur(is.type))
        )

        if (Arr.isArrayNonEmpty(indexSignatures)) {
          if (r.propertySignatures.length === 0 && indexSignatures.length === 1) {
            return indexSignatures[0]
          }
          return Schema.StructWithRest(Schema.Struct(fields), indexSignatures)
        }

        return Schema.Struct(fields)
      }
      case "Union": {
        if (r.types.length === 0) return Schema.Never
        if (r.types.every((t) => t._tag === "Literal")) {
          if (r.types.length === 1) {
            return Schema.Literal(r.types[0].literal)
          }
          return Schema.Literals(r.types.map((t) => t.literal))
        }
        return Schema.Union(r.types.map(recur), { mode: r.mode })
      }
    }
  }

  function toSchemaChecks(top: Schema.Top, schema: Representation): Schema.Top {
    switch (schema._tag) {
      default:
        return top
      case "String":
      case "Number":
      case "BigInt":
      case "Arrays":
      case "Objects":
      case "Declaration": {
        const checks = schema.checks.map(toSchemaCheck)
        return Arr.isArrayNonEmpty(checks) ? top.check(...checks) : top
      }
    }
  }

  function toSchemaCheck(check: Check<Meta>): SchemaAST.Check<any> {
    switch (check._tag) {
      case "Filter":
        return toSchemaFilter(check)
      case "FilterGroup": {
        return Schema.makeFilterGroup(Arr.map(check.checks, toSchemaCheck), check.annotations)
      }
    }
  }

  function toSchemaFilter(filter: Filter<Meta>): SchemaAST.Check<any> {
    const a = filter.annotations
    switch (filter.meta._tag) {
      // String Meta
      case "isStringFinite":
        return Schema.isStringFinite(a)
      case "isStringBigInt":
        return Schema.isStringBigInt(a)
      case "isStringSymbol":
        return Schema.isStringSymbol(a)
      case "isMinLength":
        return Schema.isMinLength(filter.meta.minLength, a)
      case "isMaxLength":
        return Schema.isMaxLength(filter.meta.maxLength, a)
      case "isLengthBetween":
        return Schema.isLengthBetween(filter.meta.minimum, filter.meta.maximum, a)
      case "isPattern":
        return Schema.isPattern(filter.meta.regExp, a)
      case "isTrimmed":
        return Schema.isTrimmed(a)
      case "isUUID":
        return Schema.isUUID(filter.meta.version, a)
      case "isGUID":
        return Schema.isGUID(a)
      case "isULID":
        return Schema.isULID(a)
      case "isBase64":
        return Schema.isBase64(a)
      case "isBase64Url":
        return Schema.isBase64Url(a)
      case "isStartsWith":
        return Schema.isStartsWith(filter.meta.startsWith, a)
      case "isEndsWith":
        return Schema.isEndsWith(filter.meta.endsWith, a)
      case "isIncludes":
        return Schema.isIncludes(filter.meta.includes, a)
      case "isUppercased":
        return Schema.isUppercased(a)
      case "isLowercased":
        return Schema.isLowercased(a)
      case "isCapitalized":
        return Schema.isCapitalized(a)
      case "isUncapitalized":
        return Schema.isUncapitalized(a)

      // Number Meta
      case "isFinite":
        return Schema.isFinite(a)
      case "isInt":
        return Schema.isInt(a)
      case "isMultipleOf":
        return Schema.isMultipleOf(filter.meta.divisor, a)
      case "isGreaterThan":
        return Schema.isGreaterThan(filter.meta.exclusiveMinimum, a)
      case "isGreaterThanOrEqualTo":
        return Schema.isGreaterThanOrEqualTo(filter.meta.minimum, a)
      case "isLessThan":
        return Schema.isLessThan(filter.meta.exclusiveMaximum, a)
      case "isLessThanOrEqualTo":
        return Schema.isLessThanOrEqualTo(filter.meta.maximum, a)
      case "isBetween":
        return Schema.isBetween(filter.meta, a)

      // BigInt Meta
      case "isGreaterThanBigInt":
        return Schema.isGreaterThanBigInt(filter.meta.exclusiveMinimum, a)
      case "isGreaterThanOrEqualToBigInt":
        return Schema.isGreaterThanOrEqualToBigInt(filter.meta.minimum, a)
      case "isLessThanBigInt":
        return Schema.isLessThanBigInt(filter.meta.exclusiveMaximum, a)
      case "isLessThanOrEqualToBigInt":
        return Schema.isLessThanOrEqualToBigInt(filter.meta.maximum, a)
      case "isBetweenBigInt":
        return Schema.isBetweenBigInt(filter.meta, a)

      // Object Meta
      case "isMinProperties":
        return Schema.isMinProperties(filter.meta.minProperties, a)
      case "isMaxProperties":
        return Schema.isMaxProperties(filter.meta.maxProperties, a)
      case "isPropertiesLengthBetween":
        return Schema.isPropertiesLengthBetween(filter.meta.minimum, filter.meta.maximum, a)
      case "isPropertyNames":
        return Schema.isPropertyNames(recur(filter.meta.propertyNames) as Schema.Record.Key, a)

      // Arrays Meta
      case "isUnique":
        return Schema.isUnique(a)

      // Date Meta
      case "isDateValid":
        return Schema.isDateValid(a)
      case "isGreaterThanDate":
        return Schema.isGreaterThanDate(filter.meta.exclusiveMinimum, a)
      case "isGreaterThanOrEqualToDate":
        return Schema.isGreaterThanOrEqualToDate(filter.meta.minimum, a)
      case "isLessThanDate":
        return Schema.isLessThanDate(filter.meta.exclusiveMaximum, a)
      case "isLessThanOrEqualToDate":
        return Schema.isLessThanOrEqualToDate(filter.meta.maximum, a)
      case "isBetweenDate":
        return Schema.isBetweenDate(filter.meta, a)

      // Size Meta
      case "isMinSize":
        return Schema.isMinSize(filter.meta.minSize, a)
      case "isMaxSize":
        return Schema.isMaxSize(filter.meta.maxSize, a)
      case "isSizeBetween":
        return Schema.isSizeBetween(filter.meta.minimum, filter.meta.maximum, a)
    }
  }
}