Shapes & Validation¶
Shapes define the structure of data flowing through your Trails application. They map to SHACL node shapes and can be exported as Turtle for interoperability with other RDF tools.
The @shape decorator¶
Use @shape on a class to register it as a Trails shape. Fields inside the
class are declared with predicate().
Three call forms¶
from trails import shape, predicate
# 1. Bare — IRI auto-minted from trails.toml [project].name + class name
# Result: trails://<project>/Note
@shape
class Note:
title: str = predicate("schema:name")
# 2. Positional IRI
@shape("https://myapp.example/ns/Person")
class Person:
name: str = predicate("schema:name", required=True)
# 3. Keyword IRI (equivalent to positional)
@shape(iri="https://myapp.example/ns/Person")
class Person:
name: str = predicate("schema:name", required=True)
Auto-minted IRIs¶
When you omit iri=, Trails reads [project].name from trails.toml and
produces trails://<project>/<ClassName>. If no config file is found, the
project segment defaults to "local". The trails:// scheme is deliberately
distinct from https: -- pass iri= explicitly when you need a canonical
web IRI.
Additional decorator kwargs¶
| Kwarg | Type | Purpose |
|---|---|---|
iri |
str |
Explicit shape IRI |
extends |
list |
Parent shape/node-type IRIs or classes (maps to rdfs:subClassOf) |
prefixes |
dict[str, str] |
Namespace prefix mappings for predicate IRIs |
predicate() -- full kwargs reference¶
predicate(iri, *, ...) declares an RDF predicate binding. The first
positional argument is the predicate IRI (e.g. "schema:name").
Cardinality kwargs¶
| Kwarg | Type | Default | SHACL | Description |
|---|---|---|---|---|
min |
int \| None |
1 |
sh:minCount |
Minimum number of values |
max |
int \| None |
None |
sh:maxCount |
Maximum number of values (None = unbounded) |
Convenience aliases: required and many¶
Two aliases provide a more readable API for common cardinality patterns:
| Alias | Maps to | Example |
|---|---|---|
required=True |
min=1 |
Field must have at least one value |
required=False |
min=0 |
Field is optional |
many=True |
max=None |
Unbounded (default anyway) |
many=False |
max=1 |
At most one value |
Conflict detection. You can pass both an alias and the explicit kwarg
only when they agree. Disagreements raise ValueError:
# OK -- they agree
name = predicate("schema:name", required=True, min=1)
# ValueError: conflicting required=True and min=0
name = predicate("schema:name", required=True, min=0)
# ValueError: conflicting many=False and max=5
tags = predicate("schema:keywords", many=False, max=5)
Value-constraint kwargs¶
| Kwarg | Type | Default | SHACL | Description |
|---|---|---|---|---|
one_of |
list \| tuple |
None |
sh:in |
Enumeration of allowed literal values |
min_value |
int \| float |
None |
sh:minInclusive |
Minimum numeric value (inclusive) |
max_value |
int \| float |
None |
sh:maxInclusive |
Maximum numeric value (inclusive) |
pattern |
str |
None |
sh:pattern |
Regex the string value must match |
min_length |
int |
None |
sh:minLength |
Minimum string length |
max_length |
int |
None |
sh:maxLength |
Maximum string length |
All value-constraint kwargs default to None (unconstrained) and are
additive -- omitting them preserves backward compatibility.
Type annotations and XSD mapping¶
Type annotations on the class are automatically mapped to XSD datatypes:
| Python type | XSD datatype |
|---|---|
str |
xsd:string |
int |
xsd:integer |
float |
xsd:decimal |
bool |
xsd:boolean |
Examples¶
Simple shape¶
from trails import shape, predicate
@shape("https://example.org/PersonShape")
class Person:
name: str = predicate("schema:name", required=True, min_length=1)
email: str = predicate("schema:email", pattern=r".+@.+\..+")
age: int = predicate("schema:age", min_value=0, max_value=150)
Shape with required, many, and one_of¶
@shape
class Patient:
# Required, at most one value
name: str = predicate("schema:name", required=True, many=False, min_length=1)
# Optional, unbounded (e.g. multiple phone numbers)
phone: str = predicate("schema:telephone", required=False, many=True)
# Required, must be one of the listed values
status: str = predicate("ex:status", required=True, one_of=["active", "inactive", "discharged"])
# Optional numeric with bounds
age: int = predicate("schema:age", required=False, many=False, min_value=0, max_value=150)
Shape with extends¶
@shape(iri="https://example.org/EmployeeShape", extends=["https://example.org/PersonShape"])
class Employee:
department: str = predicate("ex:department", required=True)
badge_id: str = predicate("ex:badgeId", required=True, pattern=r"^EMP-\d{6}$")
Shape-from-annotation (capabilities)¶
The @capability decorator can auto-derive input_shape from Python type
hints on the decorated function, so you don't always need to declare shapes
manually. See the Capabilities guide for details.
Runtime introspection¶
list_shapes()¶
Returns all registered ShapeMeta objects:
from trails import list_shapes
for s in list_shapes():
print(s.iri, len(s.predicates), "predicates")
get_shape(iri)¶
Look up a single shape by IRI. Returns ShapeMeta | None:
from trails import get_shape
meta = get_shape("https://example.org/PersonShape")
if meta:
for name, pred in meta.predicates.items():
print(f" {name}: {pred.iri} (min={pred.min_count}, max={pred.max_count})")
ShapeMeta fields¶
| Field | Type | Description |
|---|---|---|
iri |
str |
The shape's canonical IRI |
cls |
type |
The decorated Python class |
extends |
list[str] |
Parent shape/node-type IRIs |
prefixes |
dict[str, str] |
Namespace prefixes |
predicates |
dict[str, PredicateInfo] |
Field name -> predicate metadata |
SHACL export¶
CLI¶
Programmatic: to_shacl_ttl(meta)¶
Generate SHACL Turtle for a single ShapeMeta:
from trails.shapes import to_shacl_ttl
from trails import get_shape
meta = get_shape("https://example.org/PersonShape")
print(to_shacl_ttl(meta))
Output:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<https://example.org/PersonShape>
a sh:NodeShape ;
sh:targetClass <https://example.org/PersonShape> ;
sh:property [
sh:path <schema:name> ;
sh:name "name" ;
sh:minCount 1 ;
sh:datatype xsd:string ;
] ;
sh:property [
sh:path <schema:email> ;
sh:name "email" ;
sh:minCount 1 ;
sh:datatype xsd:string ;
] ;
sh:property [
sh:path <schema:age> ;
sh:name "age" ;
sh:minCount 1 ;
sh:datatype xsd:integer ;
] .
Validation¶
Shapes are enforced at two levels: capability invocation and knowledge-graph writes.
Capability invocation¶
When a capability declares input_shape or output_shape, Trails validates
data automatically during invoke():
from trails import capability
@capability("classify", input_shape=Document, output_shape=Classification)
def classify_document(doc):
# doc has already been validated against Document's shape
# Return value will be validated against Classification's shape
return {"category": "research", "confidence": 0.95}
Knowledge-graph writes¶
Shapes registered against a @node_type (via extends= or same-class
decoration) are enforced when writing nodes through ctx.kg. The
validate_instance() and validate_subject() functions check every
per-field constraint (one_of, min_value, max_value, pattern,
min_length, max_length) and raise TrailsError on the first violation.
Validation modes¶
| Mode | Behavior on invalid data |
|---|---|
| Soft (default) | Log warning, proceed with invocation |
| Hard | Raise ValidationError, abort invocation |
Configure the mode in trails.toml:
Quick reference¶
from trails import shape, predicate, get_shape, list_shapes
from trails.shapes import to_shacl_ttl
@shape(iri="https://example.org/TaskShape")
class Task:
title: str = predicate("ex:title", required=True, min_length=1, max_length=200)
priority: int = predicate("ex:priority", required=True, many=False,
min_value=1, max_value=5)
status: str = predicate("ex:status", one_of=["todo", "doing", "done"])
tags: str = predicate("ex:tag", required=False, many=True)
# Introspect
assert get_shape("https://example.org/TaskShape") is not None
assert len(list_shapes()) >= 1
# Export SHACL
print(to_shacl_ttl(get_shape("https://example.org/TaskShape")))