Schema validation¶
Sphinx-Needs (≥ 6.0) includes a powerful schema validation system
based on JSON Schema.
The schema_definitions_from_json option in [needs]
points to a JSON file of validation rules
that check individual need properties and cross-need relationships.
Use this when per-field schema constraints (see Field schemas)
are not enough —
for example to enforce that certain need types carry specific fields,
or that linked needs satisfy particular conditions.
See also
- sphinx-needs schema documentation
The full sphinx-needs schema reference.
Minimal example¶
[needs]
schema_definitions_from_json = "schemas.json"
With a schemas.json alongside your ubproject.toml:
{
"schemas": [
{
"id": "spec-needs-priority",
"message": "Specifications must have a priority",
"select": {
"properties": { "type": { "const": "spec" } }
},
"validate": {
"local": { "required": ["priority"] }
}
}
]
}
Overview¶
Schema validation operates at multiple levels:
Field-level schemas — defined inline on
[needs.fields]or[needs.links], these validate every value entered for that field globally (see Field schemas).Schema definitions — defined in a
schemas.jsonfile (viaschema_definitions_from_json), these provide advanced validation including type-specific rules, conditional constraints, and cross-need (network) validation.
Both levels complement each other: field-level schemas enforce global constraints for a field, while schema definitions allow complex, type-specific validation logic.
Configuration¶
- schema_definitions_from_json
Type:
string(default:"")Path to a JSON file containing schema definitions. Available since sphinx-needs 6.0.
[needs] schema_definitions_from_json = "schemas.json"
The schemas.json file¶
The schema file has two top-level keys:
"$defs"(optional): Reusable schema components referenced via$ref"schemas"(required): Array of validation rule objects
{
"$defs": {
"type-spec": {
"properties": {
"type": { "const": "spec" }
}
},
"safe-need": {
"properties": {
"asil": { "enum": ["A", "B", "C", "D"] }
},
"required": ["asil"]
}
},
"schemas": [
{
"id": "spec-id-pattern",
"severity": "warning",
"message": "Spec IDs must be uppercase",
"select": {
"$ref": "#/$defs/type-spec"
},
"validate": {
"local": {
"properties": {
"id": { "pattern": "^SPEC_[A-Z0-9_]+$" }
}
}
}
}
]
}
Each schema object in the schemas array supports:
id(optional): Identifier for the schema rule (used in error messages)severity(optional):"violation"(default),"warning", or"info"message(optional): Custom message shown when validation failsselect(optional): JSON Schema that filters which needs this rule applies to. If omitted, the rule applies to all needs.validate(required): Validation rules with these subsections:local: Validates properties of the need itselfnetwork: Validates relationships with outgoing linked needsnetwork_back: Validates relationships with incoming linked needs
Any other key is a configuration error, reported as config.schema_unknown_key, and no
rule from the file is applied while it remains.
That covers every position ubCode reads itself: a rule, validate, select,
validate.local, a contains or items block, and a network link block.
An unknown keyword inside an allOf element is different:
ubCode honours it, so it is reported as config.schema_sphinx_needs_incompatible —
a warning that sphinx-build will refuse the file — and the rule still applies.
This is what a misspelled key would otherwise cost you silently:
a typo in select leaves the rule with no filter, so it applies to every need,
and a typo inside validate.local leaves it with no constraints, so it passes everything.
sphinx-needs refuses to build such a file at all.
A rule with no validate member is refused too, as
config.schema_missing_validate:
validate is what says what to check, so such a rule enforces nothing at all.
A validate that is present but empty is fine — it asks for nothing and gets
nothing, which is a rule doing what it says.
The accepted key set tracks the rule grammar of sphinx-needs 8.4.0. Each diagnostic lists the keys accepted at the position it fired on, so you never have to come back to this page to find them. A keyword added by a newer sphinx-needs is reported here until ubCode’s grammar is updated — loudly, rather than by quietly changing what your rule means.
Key values are checked too, where sphinx-needs constrains them,
and a wrong one refuses the file in the same way — reported as
config.schema_invalid_value:
typemust be"object"in aselect, in avalidate.localand inside anallOfelement, and"array"on a network link block.requiredmust be an array of strings. The message names the offending item and its position in the array.unevaluatedPropertiesmust be a boolean.a network link block must be an object, its
containsanditemsmust be objects, and itsminContainsandmaxContainsmust be integers. Any integer is accepted, whatever its sign or size, because that is all sphinx-needs requires: a negative bound is applied when the rule runs, not refused when it loads —maxContains: -1reports every need the rule selects, andminContains: -1is satisfied by any number of links. A value that is not an integer — a fractional number, a string, an explicitnull— is refused.
What a wrong type costs you depends on the position, and it is worth knowing
which:
In a
selector avalidate.local, ubCode ignores the value — the validator it compiles is an object schema either way — so the check is there for parity and clarity rather than to change any outcome.Inside an
allOfelement the value is compiled verbatim, so"type": "string"makes the element match no need at all (a need is an object) and the wholeselectsilently selects nothing. That is the expensive one.On a network link block ubCode also ignores the value, but the rest of the block is not ignored: before this check, a block that failed to load — a
maxContainsthat is not a number, say — was dropped whole and in silence, which both voided the constraint and hid a wrongtypebeside it.
required, unevaluatedProperties and the network-block members
used to be reported as warnings (or not at all)
while the rule ran on with that constraint quietly dropped.
They now refuse the file, because a constraint you asked for and did not get
is the same harm as a misspelled key.
An explicit null counts as a wrong value, not as an omission —
leaving a member out entirely is still fine.
Measured at sphinx-needs 8.4.0: sphinx-build aborts on every value listed above
as refused, and accepts every value listed above as accepted,
which is the rule ubCode mirrors —
a value is refused here only where sphinx-build refuses it too,
with one disclosed exception:
a minContains greater than its maxContains is refused at load,
where sphinx-build accepts the pair and instead reports
every need the rule applies to at runtime.
Exactly two values are deliberately not checked.
A rule’s idx is documented as an integer,
but sphinx-needs overwrites it with the rule’s position in the array
before it validates anything,
so a wrong one there is harmless in both engines —
measured at sphinx-needs 8.4.0, a rule carrying "idx": "x" builds green.
And the values inside a properties.<field> schema are a separate matter:
there ubCode currently only warns when a field’s declared type
contradicts its [needs.fields] definition,
where sphinx-build aborts the build —
measured at sphinx-needs 8.4.0, and tracked as a follow-up.
See the type system below
for what those types are.
Type system¶
Sphinx-Needs supports the following types for need fields,
defined via schema.type in [needs.fields]:
Primitive types:
string,boolean,integer,numberArray types:
arraywith typed items (e.g.schema = {type = "array", items = {type = "string"}})
Type information from [needs.fields] is automatically injected
into schemas.json rules,
so you don’t need to repeat "type": "string" in every schema.
If you do specify a type in the schema file,
it must match the field definition.
Local validation¶
Local validation checks properties of a single need without any information from other needs. This makes it suitable for instant feedback in IDEs.
Note
docname is readable in select and in validate.local.properties —
pattern checks against it work as you would expect —
but required: ["docname"] reports it as missing for every need,
in ubCode and in sphinx-needs alike.
Match on it; do not require it.
Example: enforce that efforts is between 0 and 20,
and that approval is required when efforts exceed 15:
{
"schemas": [
{
"id": "efforts-range",
"select": {
"properties": { "type": { "enum": ["spec", "feat"] } }
},
"validate": {
"local": {
"properties": {
"efforts": {
"minimum": 0,
"maximum": 20
}
}
}
}
},
{
"id": "approval-required-for-high-effort",
"severity": "violation",
"message": "Approval required when efforts > 15",
"select": {
"properties": {
"type": { "enum": ["spec", "feat"] },
"efforts": { "minimum": 16 }
},
"required": ["efforts"]
},
"validate": {
"local": {
"required": ["approval"]
}
}
}
]
}
The select filter narrows which needs the rule applies to.
The validate.local section uses standard JSON Schema properties:
properties, required, allOf, anyOf, oneOf, not,
unevaluatedProperties, etc.
Network validation¶
Network validation checks relationships between linked needs. After link resolution, the validator follows outgoing links and validates properties of the target needs.
Example: a safe implementation must link to at least one safe, approved specification:
{
"id": "safe-impl-links",
"message": "Safe impl must link to approved safe specs",
"select": {
"allOf": [
{ "$ref": "#/$defs/type-impl" },
{ "$ref": "#/$defs/safe-need" }
]
},
"validate": {
"network": {
"links": {
"contains": {
"local": {
"allOf": [
{ "$ref": "#/$defs/type-spec" },
{ "$ref": "#/$defs/safe-need" },
{
"properties": {
"approval": { "const": true }
}
}
]
}
},
"minContains": 1
}
}
}
}
Key network validation properties:
validate.network.<link_type>.items— schema that all linked needs must matchvalidate.network.<link_type>.contains— schema that some linked needs must matchminContains/maxContains— how many linked needs must satisfy thecontainsschemavalidate.local.properties.<link_type>.minItems/maxItems— total link count constraints (checked locally since links are ID lists)
Network validation can be nested to validate multi-hop chains (e.g. impl → spec → feat), up to 4 levels deep.
Incoming-link validation (network_back)¶
network follows a need’s outgoing links.
Its sibling network_back follows a need’s incoming links —
the needs that link to it via a given link type.
This lets a constraint be expressed from the target’s point of view,
e.g. “every requirement must be covered by at least one spec”,
stated once on the requirement rather than on every spec.
network_back reuses the exact same shape as network
(items / contains / minContains / maxContains,
and it may be nested);
only the direction of link resolution differs.
Example: warn if a requirement has no incoming links from a spec:
{
"id": "req-covered-by-spec",
"severity": "warning",
"message": "Requirement should be covered by at least one spec",
"select": {
"properties": { "type": { "const": "req" } },
"required": ["type"]
},
"validate": {
"network_back": {
"links": {
"contains": {
"local": { "properties": { "type": { "const": "spec" } } }
},
"minContains": 1
}
}
}
}
Reusable definitions ($defs)¶
Define common schema fragments in $defs and reference them with $ref:
{
"$defs": {
"type-impl": {
"properties": { "type": { "const": "impl" } }
},
"type-spec": {
"properties": { "type": { "const": "spec" } }
},
"safe-need": {
"properties": {
"asil": { "enum": ["A", "B", "C", "D"] }
},
"required": ["asil"]
},
"safe-spec": {
"allOf": [
{ "$ref": "#/$defs/safe-need" },
{ "$ref": "#/$defs/type-spec" }
]
}
},
"schemas": []
}
$ref must be the only key in the object where it appears.
Recursive references are not allowed.
Severity levels¶
Each schema rule can specify a severity:
"info"— informational, logged as a Sphinx warning"warning"— logged as a Sphinx warning"violation"— logged as a Sphinx error (default)
{
"severity": "warning",
"message": "Consider adding a priority field",
"validate": {
"local": {
"required": ["priority"]
}
}
}
Where a rule’s message appears¶
A rule’s message is your explanation of why the constraint exists,
as opposed to the validator’s own finding, which says what did not match.
Both reach the reader, on every surface:
in
ubc check’s human output, as an indentednote:line under the finding;in
ubc check --output-format json, as auser_messagefield of its own, alongside the unchangedmessage— the field is omitted entirely for a rule that declares no message;in the editor’s diagnostics view, appended to the finding on the same line.
error[needs.schema_definition_violation]
--> (need)
components/drv/index.rst:10:4
ABC_REQ_BAD:id: "ABC_REQ_BAD" does not match "^ABC_REQ_[0-9]{4}$" (req-ids[0]/properties/id/pattern)
note: Requirement IDs are ABC_REQ_ followed by four digits.
The message decorates findings from the rule itself.
Findings from a nested network level do not carry it.
Validation messages can be suppressed using Sphinx’s suppress_warnings:
# In conf.py
suppress_warnings = [
"sn_schema_violation", # all violations
"sn_schema_warning", # all warnings
"sn_schema_info", # all info messages
"sn_schema_violation.local_fail", # only local failures
]
Supported field constraints¶
The following JSON Schema constraints are available per type.
These can be used both in [needs.fields] inline schemas
and in schemas.json validation rules.
String:
minLength, maxLength, pattern, format, enum, const
Integer / Number:
minimum, maximum, exclusiveMinimum, exclusiveMaximum,
multipleOf, enum, const
Boolean:
const
Array:
items (with sub-schema), minItems, maxItems,
uniqueItems, contains, minContains, maxContains
String format values:
"date", "date-time", "time", "duration",
"email", "uri", "hostname", "uuid", "regex"
Note
Regex pattern values must be cross-engine compatible
(Python, Rust, SQLite).
Avoid lookaheads, lookbehinds, backreferences,
and nested quantifiers.
Complete example¶
The following shows a ubproject.toml with typed fields
and a schemas.json that validates them:
ubproject.toml:
[needs]
id_required = true
schema_definitions_from_json = "schemas.json"
[[needs.types]]
directive = "feat"
title = "Feature"
prefix = "FEAT_"
[[needs.types]]
directive = "spec"
title = "Specification"
prefix = "SPEC_"
[[needs.types]]
directive = "impl"
title = "Implementation"
prefix = "IMPL_"
[needs.links.implements]
outgoing = "implements"
incoming = "implemented by"
[needs.fields.efforts]
schema = {type = "integer", minimum = 0, maximum = 100}
[needs.fields.approval]
schema = {type = "boolean"}
[needs.fields.asil]
schema = {type = "string", enum = ["QM", "A", "B", "C", "D"]}
schemas.json:
{
"$defs": {
"type-impl": {
"properties": { "type": { "const": "impl" } }
},
"type-spec": {
"properties": { "type": { "const": "spec" } }
},
"safe-need": {
"properties": {
"asil": { "enum": ["A", "B", "C", "D"] }
},
"required": ["asil"]
}
},
"schemas": [
{
"id": "effort-limits",
"message": "Efforts must be between 0 and 20 for specs",
"select": {
"properties": { "type": { "const": "spec" } }
},
"validate": {
"local": {
"properties": {
"efforts": { "maximum": 20 }
}
}
}
},
{
"id": "safe-impl-chain",
"message": "Safe impl must link to safe spec",
"select": {
"allOf": [
{ "$ref": "#/$defs/type-impl" },
{ "$ref": "#/$defs/safe-need" }
]
},
"validate": {
"network": {
"implements": {
"contains": {
"local": {
"allOf": [
{ "$ref": "#/$defs/type-spec" },
{ "$ref": "#/$defs/safe-need" }
]
}
},
"minContains": 1
}
}
}
}
]
}
Further reading¶
For the complete reference including all validation options, error message formats, debug tooling, and migration guides, see the sphinx-needs schema validation documentation.