Schema validation

This is the documentation of the schema validation implementation as proposed in the Github discussion #1451.

The new schema validation is a versatile, fast, safe and declarative way to enforce constraints on need items and their relations in Sphinx-Needs. It is supposed to replace the legacy needs_constraints and needs_warnings configuration options in future. See Migration from legacy validation for details on how to migrate.

Important

Strong typing system: Sphinx-Needs features a comprehensive strong typing system that extends throughout the entire application. This includes:

  • Early type validation: All need fields are validated against their defined types, with support for field defaults, link defaults, Dynamic functions, variants, needextend. Needs that do not conform to their types are not created and lead to a warning.

  • JSON export: Generated needs.json files honor the user provided types

  • Multi value extra fields: Array types for extra fields are fully supported

  • Gradual migration: Existing projects can migrate step-by-step to the new system, with string types as default for untyped fields.

  • Safety: The new type system core makes it possible to configure fields individually. Users may decide not to disallow variants / dynamic functions / defaults or needextend on certain fields. This makes it possible to build a system in which the RST code is the only source of truth. (Look at the constructor of sphinx_needs.needs_schema.FieldSchema.)

This represents a major improvement in data integrity and developer experience compared to the previous string-typed system.

Type system

The typing system features the following types for need fields:

  • Primitive types: string, boolean, integer, number

  • List types: array[string], array[boolean], array[integer], array[number]

The schema validation implementation is based on JSON schema, therefore JSON schema types are used for types and schema description.

Type representation in Python is as follows:

  • JSON string -> Python str

  • JSON boolean -> Python bool

  • JSON integer -> Python int

  • JSON number -> Python float

  • JSON array -> Python list

Schema configuration

The schema is configured in multiple places:

  • needs_fields is the place to add new fields that are then available for all need types. The type information for each field is globally set here, so it is valid for all the usages of that field for any need type. This is required for a coherent data storage, as it would be expected by a database system. If different data types are needed for the same option, it means creating a new field with a different name and type.

    Further, the schema field in needs_fields also supports setting global schema constraints for that option, that will be checked for each need type. E.g. minimum/maximum for integers, enum for strings, etc.

  • needs_links is the place to add new link options that are then available for all need types. The type for link fields is pre-set to array[string], as links are always lists of need IDs. The schema field in needs_links supports setting global schema constraints for that link option, that will be checked for each need type. E.g. minimum/maximum number of links or need id patterns. The schema defined here always runs on unresolved local needs, i.e. links are a list of IDs.

  • needs_types is the place to declare a field or link type anew for the needs of one type, in the type’s fields and links, and to make a type start from another type’s declarations, with inherits (see Fields per need type). A type’s schema only narrows the global one: the type of a field stays the same on every need type, and the type’s constraints are checked on its own needs, beside the global ones. A field or link type can also be declared on some need types alone. See Per-type configuration or schema rules? for what to declare there and what in schema rules.

  • core fields such as type, id, title, status or tags have pre-defined types that cannot be changed.

  • needs_schema_definitions and needs_schema_definitions_from_json are the places to define more complex validations that can act on need types, collection of fields and resolved network links. It is based on JSON schema, but adapted and used in a way so it fits perfectly in the Sphinx-Needs ecosystem.

Important

Type information and automatic injection

JSON schema requires type information in all schemas to actually perform validation. For example, validating an integer minimum constraint on a string field does not make sense, but, depending on the implementation, JSON schema might not complain about this.

Therefore, Sphinx-Needs automatically injects the type information from needs_fields and needs_links (as well as core field definitions) into the needs_schema_definitions when type information is not explicitly provided in the schemas.json file. If type information is provided in schemas.json, it must match the definition from needs_fields or core fields.

Per-type configuration or schema rules?

Most constraints that concern the needs of one type can be written in the type’s own entry in needs_types: its fields and links, and inherits (see Fields per need type). Such an entry sits next to the type it constrains, is easier to read than a schema rule that selects the type, and is inherited by the types that name it in inherits. Schema rules (needs_schema_definitions, schemas.json) are for what a type’s entry cannot say, listed in What still needs schema rules.

A type’s entry constrains its needs in three ways, which are checked differently:

  • a constraint keyword in a schema (enum, const, minimum, maximum, minLength, maxLength, pattern, minItems, maxItems, …) is checked by the schema validation, together with the schema rules, and a need that breaks it is reported and kept;

  • a structural declaration – a field that is not nullable on the type, or a field or link type that only some types declare – is checked when the need is created, and what does not fit it is dropped: the value, or the whole need;

  • a link type’s target_types – the need types its links may point to – is checked where the links are resolved, as a link’s [condition] is, and a link that breaks it is reported at its need and kept.

The examples below come from one project in the test suite, doc_schema_per_type, whose warnings the tests check:

ubproject.toml
"$schema" = "https://ubcode.useblocks.com/ubproject.schema.json"

[needs]
build_json = true
schema_definitions_from_json = "schemas.json"

[needs.fields.asil]
description = "Automotive Safety Integrity Level"
nullable = true
schema = { type = "string", enum = ["QM", "A", "B", "C", "D"] }

# [values]
[needs.types.req]
title = "Requirement"
prefix = "R_"

[needs.types.req.fields.status]
schema = { enum = ["draft", "approved"] }
default = "draft"

[needs.types.safety_req]
title = "Safety requirement"
prefix = "SR_"
inherits = "req"

[needs.types.spec]
title = "Specification"
prefix = "S_"

[needs.types.spec.links.links]
schema = { minItems = 1, maxItems = 2 }
# [/values]

# [type-local]
[needs.types.test]
title = "Test case"
prefix = "T_"

[needs.types.test.links.verifies]
incoming = "is verified by"
outgoing = "verifies"
schema = { minItems = 1 }
target_types = ["req"]
# [/type-local]

# [required]
[needs.types.safety_req.fields.asil]
nullable = false
schema = { enum = ["A", "B", "C", "D"] }
# [/required]
index.rst
Per-type configuration example
==============================

.. req:: Brakes engage within 100 ms
   :id: R_001
   :asil: QM

.. req:: A status outside the values of its type
   :id: R_002
   :status: done

.. safety_req:: Inherits the status values of a req
   :id: SR_001
   :asil: B
   :status: done

.. safety_req:: A safety requirement without an ASIL
   :id: SR_002

.. spec:: A spec without links
   :id: S_001

.. spec:: A spec with one link
   :id: S_002
   :links: R_001

.. spec:: A spec that tries to verify
   :id: S_003
   :links: R_001
   :verifies: R_001

.. test:: A test of two requirements
   :id: T_001
   :verifies: R_001, SR_001

.. test:: A test of a spec
   :id: T_002
   :verifies: S_002

.. test:: A test of nothing
   :id: T_003

.. test:: A test of the safety requirement that was not created
   :id: T_004
   :verifies: SR_002

.. test:: A test of a requirement without an ASIL
   :id: T_005
   :verifies: R_002
schemas.json
{
  "schemas": [
    {
      "id": "req-has-asil",
      "severity": "warning",
      "message": "a requirement states its ASIL",
      "select": { "properties": { "type": { "const": "req" } } },
      "validate": { "local": { "required": ["asil"] } }
    },
    {
      "id": "test-verifies-rated",
      "select": { "properties": { "type": { "const": "test" } } },
      "validate": {
        "network": {
          "verifies": {
            "items": {
              "local": {
                "required": ["asil"]
              }
            }
          }
        }
      }
    }
  ]
}

What a type’s entry can declare

A fields or links entry narrows a field or a link type for the needs of the type: the values allowed, a range, a length or a pattern, the number of links. inherits makes a type start from the declarations of another type.

[needs.types.req]
title = "Requirement"
prefix = "R_"

[needs.types.req.fields.status]
schema = { enum = ["draft", "approved"] }
default = "draft"

[needs.types.safety_req]
title = "Safety requirement"
prefix = "SR_"
inherits = "req"

[needs.types.spec]
title = "Specification"
prefix = "S_"

[needs.types.spec.links.links]
schema = { minItems = 1, maxItems = 2 }

A req takes only the status values draft and approved, and draft when it sets none; so does a safety_req, which inherits them; and a spec links to one or two needs. The schema validation reports a need that breaks one of these rules, and keeps it:

ERROR: Need 'SR_001' has schema violations:
  Severity:       violation
  Field:          status
  Need path:      SR_001
  Schema path:    fields > schema > properties > status > enum
  Schema message: "done" is not one of "draft" or "approved" [sn_schema_violation.field_fail]
ERROR: Need 'S_001' has schema violations:
  Severity:       violation
  Field:          links
  Need path:      S_001
  Schema path:    links > schema > properties > links > minItems
  Schema message: [] has less than 1 item [sn_schema_violation.link_fail]

The same status rule as a schema rule selects req and validates local properties, and is reported as local_fail with a schema path of <rule id>[<index>] > local > properties > status > enum. It would not apply to a safety_req, see What still needs schema rules. S_001, which has no links at all, is reported by the type’s minItems; a schema rule reports it only through required, and a local minItems alone does not (see Link count constraints).

Required fields: two strengths

nullable = false in a type’s entry requires the field on the needs of the type:

[needs.types.safety_req.fields.asil]
nullable = false
schema = { enum = ["A", "B", "C", "D"] }

A safety_req without an asil is not created: it is not rendered and not in the needs.json, and every link to it becomes a dead link:

index.rst:17: WARNING: Need could not be created: Field 'asil' is not nullable, but no value or default was given. [needs.create_need]
index.rst:43: WARNING: Need 'T_004' has unknown outgoing link 'SR_002' in field 'verifies' [needs.link_outgoing]

A schema rule’s required is the weaker form: it reports the need and keeps it, with the severity and the message the rule gives:

{
  "id": "req-has-asil",
  "severity": "warning",
  "message": "a requirement states its ASIL",
  "select": { "properties": { "type": { "const": "req" } } },
  "validate": { "local": { "required": ["asil"] } }
}
WARNING: Need 'R_002' has schema warnings:
  Severity:       warning
  Need path:      R_002
  Schema path:    req-has-asil[0] > local > required
  User message:   a requirement states its ASIL
  Schema message: "asil" is a required property [sn_schema_warning.local_fail]

A minLength in a type’s entry does not require a nullable field: a need that leaves the field unset is not validated against it. Use nullable = false when a need without the value must not exist, and a schema rule’s required when it should be reported.

When and how violations are reported

constraint keyword in a type’s schema

structural declaration

link check

examples

enum, maximum, maxLength, minItems, …

nullable = false; a field or link type of some types only

a link type’s target_types; a link’s [condition] (conditional links)

checked

by the schema validation, after the documents are read, together with the schema rules

when the need is created: while the documents are read, or when external needs are loaded

once every link is final, after the documents are read, before the schema validation

reported as

sn_schema_violation.field_fail or sn_schema_violation.link_fail, always of severity violation, with no custom message and no source location

a warning (needs.create_need, needs.import_need, needs.undeclared_for_type), with its place in the document for a need read from the documents

a warning per link (needs.link_target_type, needs.link_condition_failed), with the place in the document of the need the link is set in

the need

is kept, with its value, and listed in Schema violation report JSON file

is kept without the value, or is not created

is kept, with the link; not listed in Schema violation report JSON file

switched off by

needs_schema_validation_enabled = False, which switches off the schema rules too

not switched off by needs_schema_validation_enabled

not switched off by needs_schema_validation_enabled; silenced by suppress_warnings

external needs

checked too: a need from needs_external_needs is validated as its type declares; a type’s entry cannot leave such needs out

applied too, with warnings that give no place in a document (needs.load_external_need for a need that is not created), see Fields per need type

checked too, with warnings that give the need’s URL and no place in a document (needs.external_link_target_type, needs.link_condition_failed)

The constraint keywords of a type’s entry are suppressed with the same suppress_warnings entries as other schema findings (Suppressing validation messages): sn_schema_violation.field_fail hides every field’s, of every type at once.

What still needs schema rules

A type’s entry only says something about the needs of the type themselves, and the types of the needs their links point to. These need schema rules:

  • anything about linked needs beyond their type: their status or other fields, or “a req but not a safety_req” (Network validation); which need types a link may point to is the link type’s target_types (needs_links), which accepts subtypes;

  • how many links point to a need of a given type, such as “at least one req” among links that may point to several types: a network rule’s contains with minContains (Link count constraints); a link type’s minItems and maxItems count every link, whatever its target’s type, a dead link too;

  • incoming links, such as “every requirement is verified by a test” (Incoming link validation (network_back));

  • conditions between fields, such as “approval is required when efforts is greater than 15”;

  • multi-hop chains (nested network validation);

  • a pattern for the IDs of one type (id cannot be declared in a type’s entry);

  • a severity or message per rule (Rule severity);

  • leaving out external or imported needs, with a select on is_external or is_import.

The target_types of verifies says that a test verifies a req, a safety_req included. That the requirements it verifies state their ASIL is about the linked needs’ fields, and takes a schema rule:

{
  "id": "test-verifies-rated",
  "select": { "properties": { "type": { "const": "test" } } },
  "validate": {
    "network": {
      "verifies": {
        "items": {
          "local": {
            "required": ["asil"]
          }
        }
      }
    }
  }
}
ERROR: Need 'T_005' has schema violations:
  Severity:       violation
  Need path:      T_005 > verifies > R_002
  Schema path:    test-verifies-rated[1] > validate > network > verifies > items > local > required
  Schema message: "asil" is a required property [sn_schema_violation.network_local_fail]

The header names the need the rule selects, T_005, and the need path gives the chain: the verifies link of T_005 to R_002, the need without an ASIL.

Important

A schema rule matches the type it names literally: it does not follow inherits. With "type": { "const": "req" } in its local properties, a test verifying a safety_req would be reported. To restrict the types of the needs a link points to, give the link type target_types instead, which accepts the types inheriting from those it lists; use a rule for what it cannot say, such as “a req but not a safety_req”, listing the types literally.

The next sections guide through an example and how to use the type and schema system to enforce constraints on need items and links between them.

Modeling example

Imagine the following modeling of need items:

../_images/01_basic_setup.drawio.png

There are a few things to note about this setup:

  • the fields efforts, approval and asil (for Automotive Security Integrity Level) are typed

  • the assigned fields differ between need types

  • the fields may be optional for a need type, required or even not allowed (this example uses schema rules throughout; much of it can also be declared in the need types’ own entries, see Per-type configuration or schema rules?)

  • some validation rules are local to the need itself, while others require information from other needs (network validation)

  • the needs link to each other in a specific way, so a safe impl can only link to a safe spec which can only link to a safe feat item

The schema validation in Sphinx-Needs allows you to define declarative rules for validating need items based on their types, properties and relationships.

This includes both local checks (validating properties of a single need) and network checks (validating relationships between multiple needs). In local checks links to other needs are just a list of string need IDs while network checks run on the data of the linked need.

The distinction is especially important in an IDE context, where local checks can provide instant feedback while network requires building the full network index first.

Note

The full example for below configuration can be found in the tests. It can directly be executed with:

packages/sphinx-needs/tests/doc_test/doc_schema_example $ uv run sphinx-build -b html . _build

Here are the full example files as a reference:

conf.py
project = "Docs modeling example"

extensions = ["sphinx_needs"]

templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]

needs_from_toml = "ubproject.toml"

html_theme = "alabaster"
index.rst
Docs Example
============

.. feat:: Feature
   :id: FEAT_SAFE
   :asil: B

.. spec:: Specification
   :id: SPEC_SAFE
   :efforts: 20
   :approval: yes
   :asil: B
   :details: FEAT_SAFE

.. impl:: Implementation
   :id: IMPL_SAFE
   :asil: B
   :links: SPEC_SAFE
ubproject.toml
"$schema" = "https://ubcode.useblocks.com/ubproject.schema.json"

[needs]
id_required = true
id_regex = "^[A-Z0-9_]{3,}"
build_json = true
schema_definitions_from_json = "schemas.json"
schema_debug_active = true
# schema_debug_ignore = []

[needs.fields.efforts]
description = "FTE days"
schema.type = "integer"
schema.minimum = 0

[needs.fields.asil]
description = "Automotive Safety Integrity Level"
schema.type = "string"
schema.enum = ["QM", "A", "B", "C", "D"]

[needs.fields.approval]
description = "Approval flag"
schema.type = "boolean"

[needs.links.links]
outgoing = "links"
incoming = "linked by"
schema.maxItems = 1

[needs.links.details]
outgoing = "details"
incoming = "detailed by by"
schema.maxItems = 1

[[needs.types]]
directive = "feat"
title = "Feat"
prefix = "Feat_"

[[needs.types]]
directive = "spec"
title = "Specification"
prefix = "SPEC_"

[[needs.types]]
directive = "impl"
title = "Implementation"
prefix = "IMPL_"

# link modeling impl -[links]-> spec -[details]-> feat
schemas.json
{
  "$defs": {
    "type-feat": {
      "properties": {
        "type": { "const": "feat" }
      }
    },
    "type-spec": {
      "properties": {
        "type": { "const": "spec" }
      }
    },
    "type-impl": {
      "properties": {
        "type": { "const": "impl" }
      }
    },
    "safe-feat": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-feat" }
      ]
    },
    "safe-spec": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-spec" }
      ]
    },
    "safe-impl": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-impl" }
      ]
    },
    "safe-need": {
      "properties": {
        "asil": { "enum": ["A", "B", "C", "D"] }
      },
      "required": ["asil"]
    },
    "high-efforts": {
      "properties": {
        "efforts": { "minimum": 15 }
      },
      "required": ["efforts"]
    }
  },
  "schemas": [
    {
      "severity": "warning",
      "message": "id must be uppercase with numbers and underscores",
      "validate": {
        "local": { "properties": { "id": { "pattern": "^[A-Z0-9_]+$" } } }
      }
    },
    {
      "id": "impl",
      "select": {
        "$ref": "#/$defs/type-impl"
      },
      "validate": {
        "local": {
          "properties": {
            "id": { "pattern": "^IMPL_[a-zA-Z0-9_-]*$" },
            "links": {},
            "asil": {}
          },
          "unevaluatedProperties": false
        }
      }
    },
    {
      "id": "spec",
      "select": {
        "$ref": "#/$defs/type-spec"
      },
      "validate": {
        "local": {
          "properties": {
            "id": { "pattern": "^SPEC_[a-zA-Z0-9_-]*$" },
            "efforts": { "minimum": 0, "maximum": 20 },
            "approval": {},
            "asil": {},
            "details": {}
          },
          "unevaluatedProperties": false
        }
      }
    },
    {
      "id": "feat",
      "select": {
        "$ref": "#/$defs/type-feat"
      },
      "validate": {
        "local": {
          "properties": {
            "id": { "pattern": "^FEAT_[a-zA-Z0-9_-]*$" },
            "efforts": { "minimum": 0, "maximum": 20 },
            "approval": {},
            "asil": {},
            "details": {}
          },
          "unevaluatedProperties": false
        }
      }
    },
    {
      "id": "spec-approval-required",
      "severity": "violation",
      "message": "Approval required due to high efforts",
      "select": {
        "allOf": [
          { "$ref": "#/$defs/high-efforts" },
          { "$ref": "#/$defs/type-spec" }
        ]
      },
      "validate": {
        "local": {
          "required": ["approval"]
        }
      }
    },
    {
      "id": "spec-approval-not-given",
      "severity": "warning",
      "message": "Approval not given",
      "select": {
        "allOf": [
          { "$ref": "#/$defs/type-spec" },
          { "$ref": "#/$defs/high-efforts" }
        ]
      },
      "validate": {
        "local": {
          "properties": {
            "approval": { "const": true }
          },
          "required": ["approval"]
        }
      }
    },
    {
      "id": "safe-spec-[details]->safe-feat",
      "message": "Safe spec details safe feat",
      "select": {
        "$ref": "#/$defs/safe-spec"
      },
      "validate": {
        "network": {
          "details": {
            "contains": {
              "local": {
                "$ref": "#/$defs/safe-feat"
              }
            },
            "minContains": 1,
            "maxContains": 1
          }
        }
      }
    },
    {
      "id": "safe-impl-[links]->safe-spec",
      "message": "Safe impl links to safe spec",
      "select": {
        "$ref": "#/$defs/safe-impl"
      },
      "validate": {
        "network": {
          "links": {
            "contains": {
              "local": {
                "$ref": "#/$defs/safe-spec"
              }
            },
            "minContains": 1
          }
        }
      }
    }
  ]
}

Field configuration

Above modeling can be reached with the following ubproject.toml configuration:

"$schema" = "https://ubcode.useblocks.com/ubproject.schema.json"

[needs]
id_required = true
id_regex = "^[A-Z0-9_]{3,}"
build_json = true
schema_definitions_from_json = "schemas.json"
schema_debug_active = true
# schema_debug_ignore = []

[needs.fields.efforts]
description = "FTE days"
schema.type = "integer"
schema.minimum = 0

[needs.fields.asil]
description = "Automotive Safety Integrity Level"
schema.type = "string"
schema.enum = ["QM", "A", "B", "C", "D"]

[needs.fields.approval]
description = "Approval flag"
schema.type = "boolean"

[needs.links.links]
outgoing = "links"
incoming = "linked by"
schema.maxItems = 1

[needs.links.details]
outgoing = "details"
incoming = "detailed by by"
schema.maxItems = 1

[[needs.types]]
directive = "feat"
title = "Feat"
prefix = "Feat_"

[[needs.types]]
directive = "spec"
title = "Specification"
prefix = "SPEC_"

[[needs.types]]
directive = "impl"
title = "Implementation"
prefix = "IMPL_"

# link modeling impl -[links]-> spec -[details]-> feat

Primary type definition

The configuration needs_fields is used to define the primary type information for fields. This type information is globally valid for all usages of that field across any need type.

For primitive types (string, integer, number, boolean):

[needs.fields.efforts]
schema.type = "integer"

[needs.fields.approval]
schema.type = "boolean"

For array types, both the array type and the item type must be specified:

[needs.fields.tags]
schema.type = "array"
schema.items.type = "string"

[needs.fields.priorities]
schema.type = "array"
schema.items.type = "integer"

Type constraints

Additional schema constraints can also be defined here, which will be validated globally:

[needs.fields.asil]
name = "asil"
schema.type = "string"
schema.enum = ["QM", "A", "B", "C", "D"]

[needs.fields.efforts]
name = "efforts"
schema.type = "integer"
schema.minimum = 0
schema.maximum = 100

The same constraints as defined in Supported data types and constraints can be used here.

Type information in schemas.json

The schemas.json file (or needs_schema_definitions) also requires type information for validation, but:

  • If type is not specified in schemas.json, it will be automatically injected from the needs.fields definition (or from core field definitions for built-in fields)

  • If type is specified in schemas.json, it must match the type defined in needs.fields (or the core field definition)

This ensures type consistency across your entire configuration while reducing duplication. The injection to the schema rules is required for safe JSON schema validation.

Schemas configuration

Schemas can be configured in two ways: directly in the conf.py file or loaded from a separate JSON file.

Python configuration (alternative)

Alternatively, schemas can be configured directly using the needs_schema_definitions configuration option in conf.py. The structure is identical to the JSON file format:

needs_schema_definitions = {
  "$defs": {
    # reusable schema components
    "type-spec": {
      "properties": {
        "type": { "const": "spec" }
      }
    }
  },
  "schemas": [
    {
      "severity": "warning",
      "message": "id must be uppercase",
      "select": {
          "$ref": "#/$defs/type-spec"
      },
      "validate": {
        "local": {
          "properties": {
            "id": { "pattern": "^SPEC_[A-Z0-9_]+$" }
          }
        }
      }
    }
  ]
}

Schema file structure reference

A schemas.json file (or needs_schema_definitions dict) has the following structure:

{
  "$defs": {
    "reusable-component-name": { /* JSON Schema definition */ }
  },
  "schemas": [
    {
      "id": "schema-identifier",  /* Used for reporting */
      "severity": "violation|warning|info",
      "message": "User-friendly error message",
      "select": { /* Selection criteria - which needs to validate */ },
      "validate": {
        "local": { /* Local validation rules */ },
        "network": { /* Outgoing-link validation rules */ },
        "network_back": { /* Incoming-link validation rules */ }
      }
    }
  ]
}

Top-level properties:

  • $defs (optional): Dictionary of reusable schema components referenced via $ref. See Schema definitions ($defs).

  • schemas (required): Array of schema objects. Each schema in this array defines validation rules that will be applied to needs.

Schema object properties:

Each object within the schemas array can contain:

  • id (optional): Unique identifier for the schema, useful for debugging

  • severity (optional): "violation" (default), "warning", or "info"

  • message (optional): Custom message shown when validation fails

  • select (optional): Criteria to filter which needs this schema applies to. Uses JSON Schema syntax for local need properties. If omitted, applies to all needs.

  • validate (required): Validation rules with two subsections:

See Schema components reference for detailed documentation of each component.

Validation concepts

Validation in Sphinx-Needs operates at two levels: local and network. Understanding the difference is critical for designing effective schemas and providing appropriate feedback timing.

Local validation

Consider the following local checks:

../_images/02_local_check.drawio.png

Local validation checks individual need properties without requiring information from other needs:

  • the efforts field

    • is of type integer

    • is optional for spec and feat and disallowed for impl

    • has a minimum value of 0

    • has a maximum value of 20

  • the approval field

    • is of type boolean

    • is optional for spec and feat and disallowed for impl

    • is required in case the field efforts has a value greater than 15; if the condition is not satisfied, the violation should be returned as violation

    • must be set to True in case the field efforts has a value greater than 15; if the condition is not satisfied, the violation should be returned as warning

  • the asil field

    • is of type string

    • has a string subtype of enum

    • can only be set to one of the following values: QM | A | B | C | D

Example local validation schema:

{
  "$defs": {
    "type-feat": {
      "properties": {
        "type": { "const": "feat" }
      }
    },
    "type-spec": {
      "properties": {
        "type": { "const": "spec" }
      }
    },
    "type-impl": {
      "properties": {
        "type": { "const": "impl" }
      }
    },
    "safe-feat": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-feat" }
      ]
    },
    "safe-spec": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-spec" }
      ]
    },
    "safe-impl": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-impl" }
      ]
    },
    "safe-need": {
      "properties": {
        "asil": { "enum": ["A", "B", "C", "D"] }
      },
      "required": ["asil"]
    },
    "high-efforts": {
      "properties": {
        "efforts": { "minimum": 15 }
      },
      "required": ["efforts"]
    }
  },
  "schemas": [
    {
      "id": "impl",
      "select": {
        "$ref": "#/$defs/type-impl"
      },
      "validate": {
        "local": {
          "properties": {
            "id": { "pattern": "^IMPL_[a-zA-Z0-9_-]*$" },
            "links": {},
            "asil": {}
          },
          "unevaluatedProperties": false
        }
      }
    },
    {
      "id": "spec",
      "select": {
        "$ref": "#/$defs/type-spec"
      },
      "validate": {
        "local": {
          "properties": {
            "id": { "pattern": "^SPEC_[a-zA-Z0-9_-]*$" },
            "efforts": { "minimum": 0, "maximum": 20 },
            "approval": {},
            "asil": {},
            "details": {}
          },
          "unevaluatedProperties": false
        }
      }
    },
    {
      "id": "feat",
      "select": {
        "$ref": "#/$defs/type-feat"
      },
      "validate": {
        "local": {
          "properties": {
            "id": { "pattern": "^FEAT_[a-zA-Z0-9_-]*$" },
            "efforts": { "minimum": 0, "maximum": 20 },
            "approval": {},
            "asil": {},
            "details": {}
          },
          "unevaluatedProperties": false
        }
      }
    },
    {
      "id": "spec-approval-required",
      "severity": "violation",
      "message": "Approval required due to high efforts",
      "select": {
        "allOf": [
          { "$ref": "#/$defs/high-efforts" },
          { "$ref": "#/$defs/type-spec" }
        ]
      },
      "validate": {
        "local": {
          "required": ["approval"]
        }
      }
    },
    {
      "id": "spec-approval-not-given",
      "severity": "warning",
      "message": "Approval not given",
      "select": {
        "allOf": [
          { "$ref": "#/$defs/type-spec" },
          { "$ref": "#/$defs/high-efforts" }
        ]
      },
      "validate": {
        "local": {
          "properties": {
            "approval": { "const": true }
          },
          "required": ["approval"]
        }
      }
    },
  ]
}

Above conditions can all be checked locally on need level which allows instant user feedback in IDE extensions such as ubCode.

Network validation

On the other hand, network checks require information from other needs:

../_images/03_network_check.drawio.png

After network resolution, the following checks can be performed:

  • a ‘safe’ impl that has an asil of A | B | C | D cannot link to spec items that have an asil of QM

  • a safe impl can only link approved spec items with link type links

  • a safe spec can only link to safe and approved feat items with link type details

  • a safe impl can link to one or more safe spec items

  • a spec can only link to exactly one feat

  • additional links to non-validating items are not allowed (that is the min/max constraints are met but there are failing additional link targets)

Example:

{
  "$defs": {
    "type-feat": {
      "properties": {
        "type": { "const": "feat" }
      }
    },
    "type-spec": {
      "properties": {
        "type": { "const": "spec" }
      }
    },
    "type-impl": {
      "properties": {
        "type": { "const": "impl" }
      }
    },
    "safe-feat": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-feat" }
      ]
    },
    "safe-spec": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-spec" }
      ]
    },
    "safe-impl": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-impl" }
      ]
    },
    "safe-need": {
      "properties": {
        "asil": { "enum": ["A", "B", "C", "D"] }
      },
      "required": ["asil"]
    },
  },
  "schemas": [
    {
      "id": "safe-spec-[details]->safe-feat",
      "message": "Safe spec details safe feat",
      "select": {
        "$ref": "#/$defs/safe-spec"
      },
      "validate": {
        "network": {
          "details": {
            "contains": {
              "local": {
                "$ref": "#/$defs/safe-feat"
              }
            },
            "minContains": 1,
            "maxContains": 1
          }
        }
      }
    },
    {
      "id": "safe-impl-[links]->safe-spec",
      "message": "Safe impl links to safe spec",
      "select": {
        "$ref": "#/$defs/safe-impl"
      },
      "validate": {
        "network": {
          "links": {
            "contains": {
              "local": {
                "$ref": "#/$defs/safe-spec"
              }
            },
            "minContains": 1
          }
        }
      }
    }
  ]
}

Network validation supports various constraints on linked needs:

Nested network validation

Network validation can be nested to validate multi-hop link chains:

{
  "id": "safe-impl-chain",
  "select": {
    "$ref": "#/$defs/safe-impl"
  },
  "validate": {
    "network": {
      "links": {
        "contains": {
          "local": {
            "$ref": "#/$defs/safe-spec"
          },
          "network": {
            "links": {
              "contains": {
                "local": {
                  "$ref": "#/$defs/safe-feat"
                }
              },
              "minContains": 1
            }
          }
        },
        "minContains": 1
      }
    }
  }
}

This validates that:

  1. A safe implementation links to safe specifications

  2. Those specifications in turn link to safe features

  3. Both link levels have minimum count requirements

Severity levels

The schema validation supports three severity levels for reporting violations:

  • info: A non-critical constraint violation indicating an informative message.

  • warning: A non-critical constraint violation indicating a warning.

  • violation : A constraint violation.

The specific severity value has no impact on the validation, but it can influence how violations are reported to the user. See Running validation and understanding errors for more details how Sphinx console output can be configured and how to suppress errors.

Schema violations are categorised with message types. Each message type is assigned a default severity of violation. The default can be overwritten in schema rules.

Schema components reference

This section provides detailed documentation of all components that can be used within a schema definition in the schemas array. These components work together to define selection criteria, validation rules, and error handling.

Select criteria

The select section defines which needs the schema applies to:

{
  "select": {
    "allOf": [
      { "$ref": "#/$defs/type-spec" },
      { "$ref": "#/$defs/high-efforts" }
    ]
  }
}

If no select is provided, the schema applies to all needs. select is always a local validation, meaning it only checks properties of the need itself. select validation also means all link fields are list of need ID strings, not need objects.

Validation rules

The validate section contains the actual validation rules:

Local validation checks individual need properties:

{
  "validate": {
    "local": {
      "properties": {
        "status": { "enum": ["open", "closed", "in_progress"] }
      },
      "required": ["status"]
    }
  }
}

local validation also means all link fields are list of need ID strings, not need objects.

Unevaluated properties control

The unevaluatedProperties property controls whether properties not explicitly defined in the schema are allowed:

{
  "validate": {
    "local": {
      "properties": {
        "status": { "enum": ["open", "closed"] }
      },
      "unevaluatedProperties": false // Only 'status' property allowed
    }
  }
}

When unevaluatedProperties: false is set and a need has additional properties, validation will report:

Schema message: Unevaluated properties are not allowed ('comment', 'priority' were unexpected)

This is useful for enforcing strict property schemas and catching typos in property names. To find out which properties are actually set, the validated needs are reduced to field values that are not on their default value.

unevaluatedProperties with allOf

The unevaluatedProperties validation also works with properties defined in allOf constructs. Properties from all schemas in the allOf array are considered as evaluated:

{
  "validate": {
    "local": {
      "properties": { "asil": {} },
      "unevaluatedProperties": false,
      "allOf": [
         { "properties": { "comment": {} } }
      ]
    }
  }
}

In this example, both asil and comment properties are considered evaluated, so only these two properties would be allowed on the need. Empty schemas for a field are allowed to mark them as evaluated. The behavior is aligned with the JSON Schema specification.

Required vs unevaluated properties

The required list has no impact on unevaluatedProperties validation. Properties listed in required must still be explicitly defined in properties or pulled in via allOf to be considered evaluated:

{
  "validate": {
    "local": {
      "properties": { "status": {} },
      "required": ["status", "priority"], // priority not in properties
      "unevaluatedProperties": false
    }
  }
}

In this case, a need with a priority property would still trigger an unevaluated properties error, even though priority is in the required list.

Rule severity

Each schema rule can specify one of the severity levels info, warning, or violation as described in Severity levels. If not given, violation is used as a default. The constraints of needs_fields, needs_links and a need type’s fields and links are not schema rules: they are always reported as violation.

The severity influences how validation failures are displayed in the console.

{
  "severity": "warning",
  "select": {
    "properties": { "type": { "const": "feat" } }
  },
  "validate": {
    "local": {
      "properties": { "id": { "pattern": "^FEAT_[a-zA-Z0-9_-]*$" } }
    }
  }
},
{
  "select": {
    "properties": { "type": { "const": "spec" } }
  },
  "validate": {
    "local": {
      "properties": { "id": { "pattern": "^SPEC_[a-zA-Z0-9_-]*$" } }
    }
  }
}

Console output for above example:

WARNING: Need 'FEAT' has schema warnings:
  Severity:       warning
  Field:          id
  Need path:      FEAT
  Schema path:    [0] > local > properties > id > pattern
  Schema message: 'FEAT' does not match '^FEAT_[a-zA-Z0-9_-]*$' [sn_schema_warning.local_fail]

ERROR: Need 'SPEC' has schema violations:
  Severity:       violation
  Field:          id
  Need path:      SPEC
  Schema path:    [1] > local > properties > id > pattern
  Schema message: 'SPEC' does not match '^SPEC_[a-zA-Z0-9_-]*$' [sn_schema_violation.local_fail]

Note how the FEAT report has a severity warning while SPEC is reported as violation. This is because the first schema explicitly set the severity to warning, while the second schema used the default violation.

Schema definitions ($defs)

Reusable schema components can be defined in the $defs section:

{
  "$defs": {
    "type-feat": {
      "properties": {
        "type": { "const": "feat" }
      }
    },
    "safe-need": {
      "properties": {
        "asil": { "enum": ["A", "B", "C", "D"] }
      },
      "required": ["asil"]
    },
    "safe-feat": {
      "allOf": [
        { "$ref": "#/$defs/safe-need" },
        { "$ref": "#/$defs/type-feat" }
      ]
    }
  }
}

A full example is outlined in the Local validation section. The key $ref must appear as the only key in the schema where it is used.

Recursive references are not allowed, i.e. a schemas cannot directly or indirectly reference itself.

Supported data types and constraints

Sphinx-Needs supports comprehensive data type validation for need options through JSON Schema. The following data types are available for need options and are defined in needs_fields. These types must be specified for fields before they can be validated in schemas.

String type

String fields store textual data with optional format validation:

{
  "properties": {
    "description": {
      "type": "string",
      "minLength": 10,
      "maxLength": 500
    }
  }
}

The following schema constraints are supported for string fields:

  • minLength / maxLength: Minimum and maximum length of the string

  • pattern: Regex pattern that the string must match (see Regex Restrictions)

  • format: Predefined string formats (see below)

  • enum: Enumerated set of allowed string values

  • const: Exact allowed string value

String formats

String fields can be validated against specific formats using the format property:

Date and time formats (ISO 8601)

{
  "properties": {
    "start_date": {"type": "string", "format": "date"},          // 2023-12-25
    "created_at": {"type": "string", "format": "date-time"},     // 2023-12-25T14:30:00Z
    "meeting_time": {"type": "string", "format": "time"},        // 14:30:00
    "project_duration": {"type": "string", "format": "duration"} // P1Y2M10DT2H30M
  }
}

Communication formats

{
  "properties": {
    "contact_email": {"type": "string", "format": "email"}, // user@example.com (RFC 5322)
    "project_url": {"type": "string", "format": "uri"},     // https://example.com (RFC 3986)
    "tracking_id": {"type": "string", "format": "uuid"}     // 123e4567-e89b-12d3-a456-426614174000 (RFC 4122)
  }
}

Enumerated values

{
  "properties": {
    "priority": {
      "type": "string",
      "enum": ["low", "medium", "high", "critical"]
    }
  }
}

Regex pattern restrictions

When using pattern for string validation, regex patterns must be compatible across Python, Rust, and SQLite engines used in the Sphinx-Needs ecosystem.

Prohibited constructs:

  • Lookaheads/Lookbehinds: (?=pattern), (?!pattern), (?<=pattern), (?<!pattern)

  • Backreferences: \1, \2, etc.

  • Nested quantifiers: (a+)+, (a*)* (can cause catastrophic backtracking)

  • Possessive quantifiers: a++, a*+ (not supported in all engines)

  • Atomic groups: (?>pattern) (not supported in all engines)

  • Recursive patterns: (?R) (not supported in all engines)

Safe patterns:

{
  "properties": {
    "id": { "pattern": "^[A-Z0-9_]+$" },           // ✓ Safe
    "version": { "pattern": "^v[0-9]+\\.[0-9]+$" }, // ✓ Safe
    "status": { "pattern": "^(open|closed)$" }      // ✓ Safe
  }
}

Unsafe patterns:

{
  "properties": {
    "id": { "pattern": "^(?=.*[A-Z]).*$" },      // ✗ Lookahead
    "ref": { "pattern": "^(\\w+)_\\1$" },        // ✗ Backreference
    "complex": { "pattern": "^(a+)+$" }          // ✗ Nested quantifiers
  }
}

The validation will reject schemas containing unsafe patterns with clear error messages.

Integer type

Integer fields store whole numbers with optional range constraints:

{
  "properties": {
    "efforts": {
      "type": "integer",
      "minimum": 0,
      "maximum": 100,
      "multipleOf": 5
    }
  }
}

The following schema constraints are supported for integer fields:

  • minimum / maximum: Minimum and maximum value of the integer

  • exclusiveMinimum / exclusiveMaximum: Exclusive minimum and maximum value

  • multipleOf: Value must be a multiple of this number

  • enum: Enumerated set of allowed integer values

  • const: Exact allowed integer value

Number type

Number fields store floating-point values:

{
  "properties": {
    "cost_estimate": {
      "type": "number",
      "minimum": 0.0,
      "exclusiveMaximum": 1000000.0
    }
  }
}

The following schema constraints are supported for number fields:

  • minimum / maximum: Minimum and maximum value of the number

  • exclusiveMinimum / exclusiveMaximum: Exclusive minimum and maximum value

  • multipleOf: Value must be a multiple of this number

  • enum: Enumerated set of allowed number values

  • const: Exact allowed number value

Boolean type

Boolean fields store true/false values with flexible input handling:

{
  "properties": {
    "approval": {"type": "boolean"},
    "is_critical": {"type": "boolean", "const": true}
  }
}

Accepted boolean values:

  • Truthy: true, yes, y, on, 1, True, Yes, On

  • Falsy: false, no, n, off, 0, False, No, Off

The enum keyword cannot be used for booleans as const is functionally equivalent and more expressive.

Array type

Array fields store lists of homogeneous typed values:

{
  "properties": {
    "tags": {
      "type": "array",
      "items": {"type": "string"},
      "minItems": 1,
      "maxItems": 10
    },
    "priorities": {
      "type": "array",
      "items": {"type": "integer"},
      "minItems": 1
    },
    "approvals": {
      "type": "array",
      "items": {"type": "boolean"}
    }
  }
}

Array properties:

  • items: Schema for all array elements (required). The dictionary can contain any of the basic type schemas outlined above. The items.type field is required.

  • uniqueItems: If set to true, all elements in the array must be unique.

  • minItems / maxItems: Array size constraints

  • contains Schema for some elements in the array. The dictionary can contain any of the basic type schemas outlined above.

  • minContains / maxContains: Constraints on number of elements matching contains;

    • If minContains is not given, it defaults to 1 when contains is present.

    • If maxContains is not given, there is no upper limit.

Running validation and understanding errors

After defining your schema configuration, running Sphinx will validate all needs against the defined schemas. This section explains the validation process and how to interpret errors.

Error messages and output

Validation errors include detailed information:

  • Severity: The severity level of the violation

  • Field: The specific field that failed validation

  • Need path: The ID of the need that failed or the link chain for network validation. Link chains are represented as in IMPL_SAFE > links > SPEC_SAFE > specifies > FEAT_SAFE, so need IDs and link types form the chain, separated by >.

  • Schema path: The JSON path within the schema that was violated. The path starts with an identifier built from the schema rule id and the 0-based index of the rule in the list. Examples:

       [0] > local > properties > id > pattern
       spec[1] > local > properties > id > pattern
    
    The first example has no schema rule ID defined, so only the index is used.
    
  • User message: Optional user-friendly message set in the schema rules.

  • Schema message: Detailed technical validation message from the validator

Example error output:

Need 'SPEC_P01' has validation errors:
  Severity:       violation
  Field:          id
  Need path:      SPEC_P01
  Schema path:    spec[1] > local > properties > id > pattern
  Schema message: 'SPEC_P01' does not match '^REQ[a-zA-Z0-9_-]*$'

For nested network validation, it can be difficult to determine which constraint and need caused the error in the chain. In such cases, the error will emit details about the failed need and the specific link that caused the issue:

WARNING: Need 'IMPL_SAFE' has validation errors:
  Severity:       violation
  Need path:      IMPL_SAFE > links
  Schema path:    safe-impl-[links]->safe-spec-[links]->safe-req[0] > validate > network > links
  User message:   Safe impl links to safe spec links to safe req
  Schema message: Too few valid links of type 'links' (0 < 1) / nok: SPEC_SAFE

    Details for SPEC_SAFE
    Need path:      IMPL_SAFE > links > SPEC_SAFE > links
    Schema path:    safe-impl-[links]->safe-spec-[links]->safe-req[0] > links > validate > network > links
    Schema message: Too few valid links of type 'links' (0 < 1) / nok: REQ_UNSAFE

      Details for REQ_UNSAFE
      Field:          asil
      Need path:      IMPL_SAFE > links > SPEC_SAFE > links > REQ_UNSAFE
      Schema path:    safe-impl-[links]->safe-spec-[links]->safe-req[0] > links > links > local > allOf > 0 > properties > asil > enum
      Schema message: 'QM' is not one of ['A', 'B', 'C', 'D'] [sn_schema.network_contains_too_few]

Severity handling

The Severity levels affect how Sphinx emits validation messages. Here is how they map to Sphinx logging levels and console output.

Schema severity info:

  • Mapped to Sphinx logging level warning

  • Displayed as WARNING: in red color in console

  • Logged with type sn_schema_info

Schema severity warning:

  • Mapped to Sphinx logging level warning

  • Displayed as WARNING: in red color in console

  • Logged with type sn_schema_warning

Schema severity violation:

  • Mapped to Sphinx logging level error

  • Displayed as ERROR: in dark red color in console

  • Logged with type sn_schema_violation

Besides the type information mentioned above, log messages also receive a subtype described in Message types.

The output format is WARNING|ERROR: <message> [type.subtype].

The combination of type and subtype can be used to selectively suppress validation messages.

Message types

The schema validation is done on local or network level. Accordingly, the following message subtypes are used to indicate which validation failed.

Local validation:

  • field_fail: Field schema validation failed (local validation, defined via schema key in needs_fields, or in a need type’s fields entry, see Fields per need type)

  • link_fail: Extra link schema validation failed (defined via schema key in needs_links, or in a need type’s links entry)

  • local_fail: Need-local validation failed (defined via validate.local in schemas of needs_schema_definitions)

Network validation:

  • network_missing_target: Outgoing link target cannot be resolved

  • network_contains_too_few: Not enough valid links (minContains failed)

  • network_contains_too_many: Too many valid links (maxContains failed)

  • network_items_fail: Linked need’s item validation failed

If not overridden in the schema rules, a default severity of violation is used for all message types. field_fail and link_fail are always of severity violation: no schema rule sets their severity.

Suppressing validation messages

Schema validation messages can be selectively suppressed using Sphinx’s suppress_warnings configuration option.

Examples:

Suppress all schema violation errors:

suppress_warnings = ["sn_schema_violation"]

Suppress all schema warnings:

suppress_warnings = ["sn_schema_warning"]

Suppress specific validation rule failures:

# Suppress only local validation failures for violations
suppress_warnings = ["sn_schema_violation.local_fail"]

# Suppress only network link count issues for warnings
suppress_warnings = ["sn_schema_warning.network_contains_too_few"]

Suppress multiple specific types:

suppress_warnings = [
    "sn_schema_violation.local_fail",
    "sn_schema_warning.field_fail",
    "sn_schema_info",  # Suppress all info messages
]

Note

Suppressing validation messages does not prevent the validation from occurring; it only hides the output. All violations will still be recorded in the schema violation report (see Schema violation report JSON file).

Schema violation report JSON file

Schema violations are also stored in a schema_violations.json file under the output directory.

Example
{
  "validation_summary": "Schema validation completed with 2 warning(s) in 0.002 seconds. Validated 1368 needs/s.",
  "validated_needs_per_second": 1368,
  "validated_needs_count": 3,
  "validation_warnings": {
    "FEAT_unsafe": [
      {
        "log_lvl": "warning",
        "type": "sn_schema",
        "subtype": "local_fail",
        "details": {
          "severity": "violation",
          "field": "id",
          "need_path": "FEAT_unsafe",
          "schema_path": "[0] > local > properties > id > pattern",
          "user_msg": "id must be uppercase with numbers and underscores",
          "validation_msg": "'FEAT_unsafe' does not match '^[A-Z0-9_]+$'"
        },
        "children": []
      }
    ],
    "IMPL_SAFE": [
      {
        "log_lvl": "warning",
        "type": "sn_schema",
        "subtype": "network_local_fail",
        "details": {
          "severity": "violation",
          "field": "asil",
          "need_path": "IMPL_SAFE > links > SPEC_UNSAFE",
          "schema_path": "safe-impl-[links]->safe-spec[7] > links > local > allOf > 0 > properties > asil > enum",
          "validation_msg": "'QM' is not one of ['A', 'B', 'C', 'D']"
        },
        "children": []
      }
    ]
  }
}

Best practices

  1. Use descriptive IDs: Give your schemas meaningful IDs for easier debugging

  2. Leverage $defs: Define reusable schema components to avoid duplication

  3. Start with warnings: Use warning severity during development, then upgrade to violation (a schema rule’s severity only: a field’s or a need type’s constraints are always violation)

  4. Provide clear messages: Include helpful message fields to guide users

  5. Test incrementally: Add schemas gradually to avoid overwhelming validation errors

  6. Use select wisely: Only apply schemas to relevant need types using select

Migration from legacy validation

The schema validation system is designed to replace the older needs_constraints and needs_warnings configuration options, offering significant advantages:

  • Declarative: JSON-based configuration instead of Python code

  • Powerful: Supports selection, local, and network validation

  • Performance: Schema validation is faster than custom validations written in Python

  • IDE support: Full IntelliSense and validation in supported editors like ubCode

  • Type safety: Strong typing with comprehensive data type support

  • Network validation: Multi-hop link validation capabilities

  • Maintainability: Easier to read, write, and version control

Migration examples

From needs_constraints:

# Old approach - needs_constraints
needs_constraints = {
    "security": {
        "check_0": "'security' in tags",
        "severity": "CRITICAL"
    },
    "critical": {
        "check_0": "'critical' in tags",
        "severity": "CRITICAL",
        "error_message": "need {{id}} does not fulfill CRITICAL constraint"
    }
}
{
  "schemas": [
    {
      "id": "security-constraint",
      "severity": "violation",
      "message": "Security needs must have security tag",
      "select": {
        "properties": {
          "tags": {
            "type": "array",
            "contains": {"const": "security"}
          }
        }
      },
      "validate": {
        "local": {
          "properties": {
            "tags": {
              "type": "array",
              "contains": {"const": "security"}
            }
          }
        }
      }
    }
  ]
}

From needs_warnings:

needs_warnings = {
    "invalid_status": "status not in ['open', 'closed', 'done']",
}
{
  "schemas": [
    {
      "id": "valid-status",
      "severity": "warning",
      "message": "Status must be one of the allowed values",
      "validate": {
        "local": {
          "properties": {
            "status": {
              "enum": ["open", "closed", "done"]
            }
          },
          "required": ["status"]
        }
      }
    }
  ]
}

Network validation benefits

The schema system provides capabilities not available in the legacy system:

{
  "schemas": [
    {
      "id": "safe-implementation-links",
      "message": "Safe implementations must link to approved specifications",
      "select": {
        "allOf": [
          {"$ref": "#/$defs/type-impl"},
          {"$ref": "#/$defs/safety-critical"}
        ]
      },
      "validate": {
        "network": {
          "links": {
            "contains": {
              "local": {
                "allOf": [
                  {"$ref": "#/$defs/type-spec"},
                  {"properties": {"approval": {"const": true}}}
                ]
              }
            },
            "minContains": 1
          }
        }
      }
    }
  ]
}

This type of multi-need relationship validation was not possible with the legacy constraint and warning systems.

Recommended migration path

  1. Audit existing constraints and warnings: Review your current validation rules

  2. Start with local validations: Convert simple property checks first

  3. Leverage network validation: Replace complex Python logic with declarative schemas

  4. Test incrementally: Validate schemas work as expected before removing legacy rules

  5. Update documentation: Ensure team members understand the new validation approach

Learning from examples

The Sphinx-Needs test suite contains comprehensive examples demonstrating schema validation in real-world scenarios. These tests are excellent learning resources showing various validation patterns and their expected outcomes.

Test fixtures

The YAML-based test fixtures contain complete, self-contained project examples with:

  • Full conf.py or ubproject.toml configuration

  • Schema definitions in schemas.json

  • Sample RST files demonstrating the schemas in action

  • Various validation scenarios (passing and failing)

Each fixture is a working example you can study and adapt for your own projects.

Snapshot testing results

The tests use snapshot testing to verify validation behavior. The expected outputs and error messages can be found in the test snapshots.

These snapshots show:

  • Expected validation errors for specific scenarios

  • Error message formatting and content

  • Different severity levels and their output

  • Network validation chain details

By examining both the fixtures (input) and snapshots (expected output), you can understand how schemas work in practice and what validation errors look like for different scenarios.