JSON Schema

Status: implemented

toJsonSchema(s) writes a schema as a JSON Schema of draft 2020-12, the form an MCP server advertises as a tool's inputSchema.

toJsonSchema({ id: bigint, note: or(option, string) })
// { "type": "object",
//   "properties": { "id": { "type": "integer" }, "note": { "type": "string" } },
//   "required": ["id"],
//   "additionalProperties": { "not": {} } }

It goes through the data form, so two spellings of one set usually write one JSON Schema (the exceptions are the data form's own known limits), a union's members come in canonical order, and a recursive schema is written once under $defs and referred to with $ref.

rttiJSON Schema
boolean, number, string{ "type": … }
bigint{ "type": "integer" }
unknown{}
never, the constant undefined{ "not": {} }
a constant{ "const": … }
a structproperties, required for the keys excluding absence, additionalProperties for the rest
a tupleprefixItems, minItems for the positions excluding absence, items for the rest
array(t), record(t)items, additionalProperties
or(…)anyOf, or one type when the union is a whole kind

JSON cannot write every value a schema describes, so the conversion is lossy. These are the cases:

  • a bigint becomes an integer, and a bigint constant the nearest double: 9007199254740993n is written 9007199254740992;
  • -0 is a constant of its own to rtti, but JSON Schema compares numbers by value, so toJsonSchema(-0), { "const": -0 }, admits 0 as well;
  • undefined has no JSON value, so the constant becomes { "not": {} };
  • NaN and the infinities are numbers to rtti and not JSON values at all;
  • JSON has no holes, so absence is kept only at the end of a tuple: [or(option, number), string] admits [, 'x'], and its JSON Schema requires both positions ("minItems": 2).

Examples

The demo on this page runs the Lossy integers lesson: { id: bigint, max: 9007199254740993n, note: or(option, string) }, whose JSON Schema has "type": "integer" for id, the constant 9007199254740992 for max, and only id and max under required. The lesson is defined in sample.f.mjs, and the module page's demo lists it among all the others.

Reference

Where it is implemented

toJsonSchema and dataToJsonSchema in media/json/schema, which also declares JSON Schema itself as an rtti schema.

Demo

JSON has no bigint: bigint becomes "integer", and the constant 9007199254740993n becomes 9007199254740992, the nearest number. note admits absence, so it is not required.

{ id: bigint, max: 9007199254740993n, note: or(option, string) }
{} [note] → or{}idmaxnotebigint9007199254740993nororoptionstring

TypeScript:

{readonly"id":bigint,readonly"max":9007199254740993n,readonly"note"?:string}

JSON Schema:

{"type":"object","properties":{"id":{"type":"integer"},"max":{"const":9007199254740992},"note":{"type":"string"}},"required":["id","max"],"additionalProperties":{"not":{}}}
Canonical form (toData)
export default [{},{"object":[{"props":{"id":{"bigint":true},"max":{"bigint":[9007199254740993n]},"note":{"unit":16,"string":true}},"rest":{}}]}];

parse · ok

export default {"id":1n,"max":9007199254740993n};

validate · ok

export default {"id":1n,"max":9007199254740993n};

Emergent Testing

Idle. Press Run to start the suite.

  • ./proof.f.mjs