A modest proposal for a revamped SHACL syntax This proposal rearranges the SHACL constructs, collapsing constraints and shapes into one construct. The result is a more regular SHACL syntax with a simpler metamodel. A few constructs become a bit more verbose. Syntax The main SHACL construct is a shape (sh:Shape). Shapes have zero or more scopes (triples with the shape as subject and sh:scopeNode, sh:scopeClass, sh:scopePropertyObject, sh:scopePropertySubject, sh:scopeAllObjects, or sh:scopeAllSubjects as the predicate), zero or more filters (values of sh:filter) which are themselves shapes, one or more components (triples with the shape as subject and one of the component properties as property, including associated triples as necessary), and can be closed (sh:closed value true, zero or more fillers of sh:ignoredProperty). Note: The shape components might be called constraints. I didn't use constraint so as not to cause confusion with the current constraints in SHACL. The syntax of the various non-template components are: sh:class class sh:classIn ( class ... ) sh:datatype datatype sh:datatypeIn ( datatypes ... ) sh:directType class sh:minLength non-negative integer sh:maxLength non-negative integer sh:minExclusive literal sh:minInclusive literal sh:maxExclusive literal sh:maxInclusive literal sh:nodeKind sh:BlankNode or sh:IRI or sh:Literal sh:pattern pattern or ( pattern flags ) sh:equals ( path ... path ) sh:notEquals ( path ... path ) sh:lessThan ( path ... path ) sh:lessThanOrEqual ( path ... path ) sh:fillers ( path shape ) sh:list shape sh:shape shape sh:and ( shape ... shape ) sh:or ( shape ... shape ) sh:not shape sh:minCount non-negative integer sh:maxCount non-negative integer sh:uniqueLang boolean sh:partition ( shape ... ) sh:sparql string Templates are invoked by using the template as a property with the template argument structure as its filler, as in tc:lang "de" or tc:predicateLang [ ex:predicate ex:germanLabel ; ex:lang "de" ] or tc:allIn ( ex:v1 ex:v2 ex:v3 ) A path is either a property or a node with an sh:inverse link to a property or a list of these. Ambiguous constructions are not allowed. Note: Qualified cardinalities are replaced by an embedded shape where the embedded shape's filter has the same role as the filler for sh:qualifiedValueShape. A vital aspect of this syntax is that each component of shapes uses exactly one triple with the shape as subject. The sole exception is closure, and closure could be reworked this way as well, but closure is special in the semantics so it is not so bad to make it special in the syntax as well. Changes Shapes and constraints are merged. This means that shapes can have the constructs that used to be part of constraints. Property constraints are refactored into the property information and a shape. Those parts of constraints that involved multiple triples (e.g., qualified cardinality constraints) are now a single triple in the constraint whose filler has all the required information. In the end, this is not a major change. Examples sh:personShape a sh:Shape; sh:scopeClass ex:Person ; sh:fillers ( name [ a sh:Shape; sh:datatype xs:string ] ) ; sh:fillers ( child [ a sh:Shape; sh:class ex:Person ] ) ; sh:fillers ( age [ a sh:Shape; sh:datatype xs:integer; sh:minCount 1 ; sh:maxCount 1 ] ) . sh:personShape is satisfied on a graph if all instances of ex:Person have all their stated names be strings, all their stated children belonging to ex:Person, and have exactly one stated age, which is an integer. Note: Combining the path and the shape would shorten the syntax but does complicate the metamodel. Alternatively sh:fillers coud take a two-element list. sh:SJG a sh:Shape; sh:scopeClass ex:Person ; sh:filter [ sh:fillers ( gender [ sh:in ( ex:female ) ] ) ] ; sh:filter [ sh:fillers ( ( child child ) [ a sh:Shape; sh:minCount 1 ] ) ] ; sh:fillers ( child [ sh:filter [ sh:fillers ( gender [ sh:in ( ex:male ) ] ) ] ; sh:class ex:Professional ] ) . sh:SJG is satisfied on a graph if all instances of ex:Person (the scope) that have ex:female as gender (the first filter) and have at least one grandchild (the second filter) have all their male children be instances of ex:Professional. Semantics (ignoring recursion) A graph satisfies a shape if the set of nodes of the graph selected by any scope of the shape satisfies the shape. A sh:scopeNode filler selects that node. A sh:scopeClass filler selects each node in the graph that is an instance of the class. A sh:scopePropertyObject filler selects each node in the graph that is an object for that property. A sh:scopePropertySubject selects each node in the graph that is a subject for that property. A shape satisfies a set of nodes (the input nodes) as follows. The input nodes that satisfy each of the filters of the shape are called in-filter nodes, those that do not are out-of-filter nodes. Some components (those involving sh:shape, sh:and, sh:or, sh:not, sh:minCount, sh:maxCount, sh:uniqueLang, and sh:partition) work on the set of in-filter nodes as a whole. If a component of this kind is not satisfied then each of the in-filter nodes fails to satisfy the shape. Some components (values of sh:class, ..., sh:pattern, sh:fillers) work on each in-filter node independently. Each in-filter node that fails to satisfy one or more of these components fails to satisfy the shape. If the shape is closed an in-filter node fails to satisfy the shape if it has a filler for some property that is neither the path of some component of the shape nor a filler for sh:ignoredProperty. Each in-filter node that does not fail to satisfy the shape is said to satisfy the shape. A shape is satisfied on a set of input nodes if there are no in-filter nodes that fail to satisfy the shape on these input nodes. The components work as follows: sh:class class - the node belongs to class ... sh:pattern pattern - the "name" of the node matches pattern sh:equals ( path ... ) - the node has the same fillers for each path sh:disjoint ( path ... ) - the fillers for the paths are pairwise disjoint sh:lessThan ( path ... ) - the fillers for a path are smaller than the fillers for the next path ... sh:fillers ( path shape ) - the fillers of path for the node satisfy each shape sh:list shape - the nodes in the list are the transitive-reflexive closure of rdf:rest - each such node has a single filler for rdf:rest, except rdf:nil which has none - each such node has a single filler for rdf:first, except rdf:nil which has none - the nodes that are rdf:first fillers sh:shape shape - the set of in-filter nodes satisfies the shape sh:and ( shape ... shape ) - the set of in-filter nodes satisfies each shape sh:or ( shape ... shape ) - each in-filter node is individually satisfied by some shape, i.e., if there are only sh:class constructs then each in-filter node belongs to one of them or the set of in-filter nodes satisfies some shape, i.e., if there are n fillers and a sh:minCount of n then the sh:or is satisfied sh:not shape - the set of in-filter nodes does not satisfy the shape sh:minCount int - there are at least int in-filter nodes sh:maxCount int - there are at most int in-filter nodes sh:uniqueLang true - only one in-filter node for any particular language tag sh:partition ( shape_1, ..., shape_n ) - let input_1 be the set of in-filter nodes - let input_i+1 be the out-of-filter nodes of shape_i on input_i - shape_i is satisfied on input_i, for 1<=i<=n - input_n+1 is empty sh:sparql string - string is a SPARQL query that implements a SHACL shape component