Skip to content

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

trails onto export --format turtle --output shapes.ttl

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:

[validation]
mode = "hard"  # "soft" or "hard"

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")))