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:

  1. Field-level schemas — defined inline on [needs.fields] or [needs.links], these validate every value entered for that field globally (see Field schemas).

  2. Schema definitions — defined in a schemas.json file (via schema_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 fails

  • select (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 itself

    • network: Validates relationships with outgoing linked needs

    • network_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:

  • type must be "object" in a select, in a validate.local and inside an allOf element, and "array" on a network link block.

  • required must be an array of strings. The message names the offending item and its position in the array.

  • unevaluatedProperties must be a boolean.

  • a network link block must be an object, its contains and items must be objects, and its minContains and maxContains must 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: -1 reports every need the rule selects, and minContains: -1 is satisfied by any number of links. A value that is not an integer — a fractional number, a string, an explicit null — is refused.

What a wrong type costs you depends on the position, and it is worth knowing which:

  • In a select or a validate.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 allOf element the value is compiled verbatim, so "type": "string" makes the element match no need at all (a need is an object) and the whole select silently 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 maxContains that is not a number, say — was dropped whole and in silence, which both voided the constraint and hid a wrong type beside 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, number

  • Array types: array with 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 match

  • validate.network.<link_type>.contains — schema that some linked needs must match

  • minContains / maxContains — how many linked needs must satisfy the contains schema

  • validate.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.

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 indented note: line under the finding;

  • in ubc check --output-format json, as a user_message field of its own, alongside the unchanged message — 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.