Schema composition
A project's effective schema is a plain JSON Schema 2020-12 document composed from the core schema and each enabled extension's shipped schema fragment. There is no jx-specific composition runtime: any compliant validator can validate a project offline, and editors get autocomplete for extension-contributed sections through an ordinary
"$schema"
binding.
Fragments
Two kinds of fragment feed the composition:
Core ships
@jxsuite/schema/schemas/project.core.schema.json: the core project properties (name,url,build,imports,extensions, …) plus two published$defs—JxFieldSchema(the JSON-Schema-subset field shape used by content and table schemas) andRelationshipRef(see Relationships ). The core fragment is open — closure happens in the generated entry document.Each extension ships the fragments named in its manifest 's
schemasmap. A project fragment contributes plainpropertiesfor its section keys. Fragments must be standalone-valid 2020-12 documents with their own$id.
The parser's project fragment, abbreviated — it contributes the
content
section:
{
"$id": "https://jxsuite.com/schema/ext/parser/project/v1",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"content": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"source": { "type": "string" },
"format": { "type": "string" },
"schema": {
"type": "object",
"properties": {
"properties": {
"additionalProperties": { "$ref": "https://jxsuite.com/schema/project/fields/v2" }
}
}
}
}
}
}
}
}
Note the one non-local reference: field-schema positions point at the canonical URI
https://jxsuite.com/schema/project/fields/v2
rather than a local shape. That is the open recursion point described below.
Generated entry documents
jx schema
writes two committed files into the project root, composing core with every fragment listed by the project's
extensions
. Both files are
self-contained single-resource schemas
: every referenced resource is embedded under
$defs
, and every
$ref
is a root-relative JSON Pointer into the same file — no relative
./node_modules/…
paths and no canonical URIs, so every editor and validator resolves them offline and identically. The generated
project.schema.json
of a project using only the parser (embedded resource bodies elided):
{
"$comment": "Generated by `jx schema` from project.json#/extensions — do not edit.",
"$defs": {
"Fields": {
"anyOf": [
{ "$ref": "#/$defs/project-core-v2/$defs/JxFieldSchema" },
{ "$ref": "#/$defs/project-core-v2/$defs/RelationshipRef" }
]
},
"project-core-v2": { "…": "embedded core fragment" },
"ext-parser-project-v1": { "…": "embedded parser fragment" }
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [{ "$ref": "#/$defs/project-core-v2" }, { "$ref": "#/$defs/ext-parser-project-v1" }],
"type": "object",
"unevaluatedProperties": false
}
Top-level section keys combine via
allOf
+
unevaluatedProperties: false
— 2020-12
unevaluatedProperties
sees annotations from adjacent
allOf
branches, so the entry document closes the object without any fragment needing to know its siblings. A misspelled or un-contributed top-level key fails validation.
Each
$defs
key is a slug of the embedded resource's
$id
—
https://jxsuite.com/schema/project/core/v2
becomes
project-core-v2
.
document.schema.json
is the same move for documents: the core document schema embedded as
#/$defs/v1
and referenced through
"allOf": [{ "$ref": "#/$defs/v1" }]
, with the paths union resource re-embedded as the union of extension-contributed
$paths
shapes (the parser contributes
ContentPathsSource
from its document fragment).
project.json
binds via
"$schema": "./project.schema.json"
.
The two union resources
Two positions are open recursion points where a fragment must reference the effective union without knowing what other extensions contribute:
Resource
$id |
Position | Shipped default → entry-document union |
|---|---|---|
https://jxsuite.com/schema/project/fields/v2 |
Field-schema values inside section entry schemas (content frontmatter fields, table columns). | Core
JxFieldSchema
+
RelationshipRef
; the entry adds extension field extras. |
https://jxsuite.com/schema/document/paths/v2 |
The
$paths
value of a document. |
Core source shapes plus an unknown-source branch; the entry drops that branch and unions each extension's paths shape. |
The mechanism is
$id
shadowing
: core ships a default resource under each well-known
$id
, and the generated entry document re-embeds the same
$id
with the effective union — outermost wins, with plain
$ref
s and no jx-specific runtime. Fragments just write
{ "$ref": "https://jxsuite.com/schema/project/fields/v2" }
at field positions; no local fallbacks needed.
An entry embed
shadows
the shipped default rather than extending it, so it restates the core members — otherwise they would stop validating the moment an extension contributed one. The paths union is the one place the two differ: the shipped default also accepts "a source shape from an extension I cannot see", because a default has no way to know which extensions a project enables and would otherwise report
{ "contentType": "blog" }
as an error. Your generated
document.schema.json
knows, so it drops that branch and checks
$paths
exactly — a misspelled
contentType
, or a source belonging to an extension you have not enabled, is an error instead of a route that silently builds zero pages. The override is settled while the entry document is generated, not while it is validated: each canonical-
$id
reference is rewritten to the root pointer of the entry's own embed, so the committed file states the winner outright.
$dynamicRef
looks like the textbook fit, but ajv 8.x supports
$dynamicAnchor
only at schema-resource roots — and the recursion unit here (a field schema) is not the document root.
$id
shadowing achieves the identical outermost-wins override; the behavior is verified against ajv in
packages/schema/tests/project-schemas.test.ts
.
Validation
Every consumer resolves the committed entry documents offline with no file access at all : each
$refis a root-relative JSON Pointer into the same document. Nonode_modules, no network, no editor configuration.jx validatevalidates the whole project tree: both committed entry documents for self-containment (any$refthat is a relative path, a URI, or a pointer to nothing fails),project.jsonagainst./project.schema.json, every component/page/layout against the bundled document schema, every project-local*.class.jsonagainst the class schema, and each fragment standalone. CI runs this over every project root in the repo.Where a client fetches the canonical URLs instead (they are served from jxsuite.com), it gets the shipped defaults — the degradation is under-suggestion of extension field extras, never false errors.
Root pointers rather than a
$id
-keyed compound document, because VS Code's JSON language service — which Monaco embeds, so this covers Studio too — implements neither half of compound-document resolution. It resolves
#/…
against the document root instead of the enclosing
$id
, and it fetches anything with a URI before the
#
over the network rather than matching the resource embedded in the same file. Root pointers sidestep both and cost nothing under ajv.
Regenerating
Run
jx schema
whenever the
extensions
list changes (and re-run
jx validate
after). Regeneration also happens without the CLI:
Studio regenerates the entry documents when settings are saved.
The dev server's Studio API regenerates them on demand when they are missing or older than
project.json, serving the same self-contained single-resource form to the browser (Monaco registers them as inline objects and never resolves a ref itself).
The outputs are committed artifacts — check
project.schema.json
and
document.schema.json
into the repo so editors and CI validate without a build step.
Related
CLI commands —
jx schemaandjx validateExtension anatomy — where fragments are declared
project.json — the document being validated
Project sections and settings — the behavior side of a contributed section