Skip to content

Schema derivation

Nola derives JSON Schema from extractor types, contextual parameter types and exported types in .tsi files. It uses the TypeScript checker’s resolved type, so aliases, imported types, utility types such as Partial<User> and inherited interfaces derive like their equivalent inline types.

TypeScript JSON Schema
string, number, boolean { type: "string" } …
"a" | "b", string enums { type: "string", enum: ["a", "b"] }
1 | 2, true, numeric enums { const: 1 } inside anyOf
T | null { anyOf: [T, { type: "null" }] }
A | B (objects, scalars, mixed) { anyOf: [A, B] } — a shared literal key makes it a discriminated union for validation
T[], ReadonlyArray<T> { type: "array", items: T }
[string, number?] { type: "array", prefixItems: […], items: false, minItems, maxItems }
{ a: string; b?: number }, interface, type aliases { type: "object", properties, required, additionalProperties: false }
interface X extends Y, A & B the flattened members
Partial, Required, Pick, Omit, other mapped types the resulting members
Record<string, T>, { [k: string]: T } { type: "object", additionalProperties: T }
generic types applied with arguments (Box<number>) the instantiated members
Date { type: "string", format: "date-time" }, revived to a Date
recursive types $defs + $ref
types from another file or from a package derived where they are declared; a .tsi or a view carries a named type’s value
JSDoc on a member description
JSDoc constraint tags (@format, @minimum, @minItems, …) the keyword — see Constraints

A .tsi file’s own named types are referenced by name ($defs entries carry the declared name); a type imported from another project file is referenced as <module>#<Name>, so two files may export a Node each.

Add constraints with JSDoc tags on a property signature or type alias. Each tag maps to a JSON Schema keyword. Nola includes the constraint in the schema and checks it during validation; failures appear in correction requests and validate results.

Tag Applies to Schema Validation message on failure
@minLength n / @maxLength n strings minLength / maxLength expected at least 3 characters, got 1
@pattern re strings pattern (the rest of the tag text, a JavaScript regular expression without flags) expected a string matching ^[a-z]+$, got "A B"
@format name strings format expected a valid email, got "x"
@minimum n / @maximum n numbers minimum / maximum expected a number ≥ 13, got 9
@exclusiveMinimum n / @exclusiveMaximum n numbers exclusiveMinimum / exclusiveMaximum expected a number > 0, got 0
@multipleOf n numbers multipleOf (decided exactly on decimals) expected a multiple of 0.01, got 1.005
@integer numbers type: "integer" expected an integer, got 1.5
@minItems n / @maxItems n arrays and tuples minItems / maxItems expected at least 1 item, got 0
@uniqueItems arrays and tuples uniqueItems (items compared as canonical JSON) expected unique items, found a duplicate at index 2

Formats: date-time, date, time, email, uri, uuid, ipv4, ipv6, hostname. Lengths count UTF-16 code units, as JavaScript’s length does.

Rules:

  • Kind. “Strings” includes string-literal unions and string enums; “numbers” includes numeric literals. The kind is decided on the non-null, non-undefined part of the type, so @minLength 1 on string | null constrains the string and lets null through. A tag on any other kind — an object, a boolean, a Date, a mixed union such as string | number — is NOLA2012.
  • Values. Numeric tags need a finite number; @pattern and @format need a value; @integer and @uniqueItems take none. A missing, malformed or repeated tag, or an unknown format, is NOLA2012.
  • Aliases. Tags on type Id = string live with Id; a property id: Id may add its own. Tags on an interface or class declaration itself are not constraints and are ignored, as are unrelated JSDoc tags (@see, @deprecated, @param).
  • Dialects. ~standard.jsonSchema renders the keywords for every target; OpenAPI 3.0 gets exclusiveMinimum: true beside minimum, and a constrained recursive reference is wrapped in allOf where the dialect ignores siblings of $ref.

Decision types (Choice<{ … }>, Scale<[ … ]> and Prob<{ … }>) retain their criteria through Partial, Pick, Omit and generic instantiation.

Their schemas describe the answer’s structure and include an x-nola-decision keyword with the question: { kind: "choice", criteria }, { kind: "scale", levels } or { kind: "prob", criteria? }. Decision providers read this keyword, and every supported schema dialect preserves it.

Validation checks that the label is one of the supplied choices, the distribution sums to 1 within 1e-6, the score is between 0 and levels.length - 1, and a probability is between 0 and 1. The runtime adds levels to a scale answer.

Malformed criteria produce NOLA2015 wherever the type is used. A JSDoc constraint tag on Prob produces NOLA2012.

Nola cannot derive schemas for Map, Set, Promise, RegExp, functions, methods, symbol, bigint, unknown, any or never. Tuple rest elements, number index signatures and generic types with unresolved arguments are also unsupported. Instantiate a generic, such as Box<number>, before using it.

An extractor with an unsupported type produces NOLA2002. Contextual parameters follow the configured policy. An unsupported exported type becomes an UnsupportedType value; using it produces a compile-time error with the reason.

Nola validates a reply against the resolved schema and reports all issues it finds. The correction request includes the full list, as does validate on a type value.

For a discriminated union, validation uses the shared literal key to select a branch and reports that branch’s issues. Other unions accept the first matching member; if none matches, validation reports one issue listing the alternatives. Valid Date fields are converted back to Date objects.

Schema derivation runs in nola run, nola build, nola check, the bundler plugins and the Turbopack loader. Each process uses one TypeScript language service, configured by the nearest tsconfig.json or strict NodeNext defaults if there is none.

The editor runs the same derivation during diagnostics checks and reports unsupported types at their source location. Hover and completion do not wait for derivation.