Validate a graph

The common variations on running a validation, and what each one changes.

Basic run

shifty validate --shapes shapes.ttl --data data.ttl
conforms: false — 1 violation in 2 findings

Finding 1 of 2
  target       class(ex:Person)
  severity     Violation
  shape        ex:PersonShape
  failure      at least 1 value(s) required along ex:email, found 0
  path         ex:email
  found        0 value(s) along the path; at least 1 required
  requirement  ∃[1..] ex:email

  affects      ex:bob
  value node   (the focus node itself)
  also fails   Finding 2

Finding 2 of 2
  target       class(ex:Person)
  severity     Violation
  shape        ex:PersonShape
  failure      test(datatype(xsd:string)) not satisfied
  path         ex:name
  requirement  test(datatype(xsd:string))

  affects      ex:bob
  value node   "123"^^xsd:integer
  also fails   Finding 1

notation
  ∃[m..n] p . X   between m and n values along p satisfy X
conforms, report_graph, results_text = shifty.validate(data, shapes)

Both --shapes and --data accept local paths or http(s) URLs, and both are repeatable — several files are merged into one graph before anything else happens.

An explicitly supplied shapes graph with zero triples is rejected. Omitting the second Python argument uses shapes embedded in the data graph; omitting CLI --data makes the --shapes graph serve both roles.

Omitting --data (or the second Python argument) makes the single graph play both roles. If that is not what you expect, read Shapes graphs and data graphs before going further; it is the most common source of a validation that passes for the wrong reason.

Get a machine-readable report

The default CLI output is a summary for a human. For a W3C sh:ValidationReport graph, serialized as Turtle:

shifty validate --shapes shapes.ttl --data data.ttl --report

For JSON:

shifty validate --shapes shapes.ttl --data data.ttl --format json

From Python, validate() already returns the report as an rdflib.Graph, so serialize it however you like:

conforms, report_graph, _ = shifty.validate(data, shapes)
print(report_graph.serialize(format="turtle"))

If you would rather have structured objects than an RDF graph to query, validate_algebra() returns violations as Python objects with the failing focus node, the property path, the offending value, and a stable constraint kind to branch on:

result = shifty.validate_algebra(data, shapes)
for violation in result.violations:
    for reason in violation.reasons:
        if reason.constraint_kind == shifty.ConstraintKind.Cardinality:
            print(violation.focus_node, reason.path, reason.message)

Branch on constraint_kind, never on the text of message.

Skip inference

By default a validation run first executes any SHACL-AF sh:rule entries in the shapes graph to a fixed point, and validates the extended graph. If your shapes contain no rules this costs almost nothing; if they do, and you want to validate only what is asserted:

shifty validate --shapes shapes.ttl --data data.ttl --no-infer
conforms, report, text = shifty.validate(data, shapes, infer=False)

Going the other way — keeping what inference derives — is in_place=True, covered in Inference during validation.

Validate against only some shapes

A large shapes graph often contains many independent profiles, and you want one of them. --shape-name (repeatable; --entry-shape is an alias) restricts which shapes act as entry points:

shifty validate --shapes shapes.ttl --data data.ttl \
  --shape-name http://example.org/PersonShape
conforms, report, text = shifty.validate(
    data, shapes,
    shape_names=["http://example.org/PersonShape"],
)

Only target-bearing statements owned by the named shapes select focus nodes. Helper shapes those entries reach through sh:node, sh:property, qualified value shapes, or boolean combinations are still evaluated as normal — this narrows what is checked, not what the checks mean. IRIs may be bare or in angle brackets.

Change which triples are visible

graph_mode controls the graph that property paths, class hierarchies, and SPARQL see during evaluation. It is independent of where shape definitions come from.

shifty validate --shapes shapes.ttl --data data.ttl --graph-mode union-all
conforms, report, text = shifty.validate(data, shapes, graph_mode="union-all")
  • data — focus nodes and evaluation use the data graph alone.

  • union (default) — focus nodes from data; evaluation sees data ∪ shapes.

  • union-all — both focus selection and evaluation see data ∪ shapes.

The default exists because class hierarchies and ontology axioms are usually authored alongside the shapes, and sh:class needs to traverse rdfs:subClassOf to work. Reach for data when you want a strict check that the data stands on its own; reach for union-all when the shapes graph also contains instances you intend to validate.

The shapes and data graph explanation tabulates these modes and covers the separate question of where shape definitions come from.

Choose what counts as failure

SHACL constraints carry a sh:severity. By default every severity — info, warning, and violation — makes the run non-conforming. To ignore the milder ones:

shifty validate --shapes shapes.ttl --data data.ttl --minimum-severity violation
conforms, report, text = shifty.validate(
    data, shapes, minimum_severity="violation",
)

Results below the threshold remain in the W3C report and Python result objects; the threshold controls their conforms value. The CLI’s default text and JSON summaries omit below-threshold findings.

Validate many graphs against one schema

Compiling a shapes graph — parsing, lowering, normalizing, planning — is a fixed cost paid before any data is examined, and for a large ontology it dominates a small validation. PreparedValidator pays it once:

validator = shifty.PreparedValidator(shapes)

for path in data_files:
    conforms, report, text = validator.validate(path)

This matters more than it sounds like it should: on the Brick corpus, whose models are small against a 229k-triple shapes closure, most of a per-process run’s wall clock is this setup. See Benchmarks.

Handle unsupported constructs

A few SHACL features are supported partially (see Feature support). on_unsupported decides what happens when the engine meets one:

conforms, report, text = shifty.validate(data, shapes, on_unsupported="error")

"ignore" (the default) makes a best effort and may return an unreliable answer; "error" refuses, so the problem surfaces instead of being silently absorbed. If you are validating anything you will act on, "error" is the safer default and the one to start with.

Invalid shapes are separate from on_unsupported. A malformed SHACL constraint or rule — including malformed SPARQL or an unresolved query prefix — raises an error before validation starts. It is never treated as an omitted unsupported feature.

See also