Most SKOS tutorials focus on skos:broader and skos:narrower. Hierarchies. Drill-downs. The tree you’d build in Protege.
The more useful property is skos:inScheme, and it gets about two paragraphs in most introductions.
The inheritance trap
Class hierarchies feel natural. A Database is a kind of Service. A MonitoredService is a kind of Service with extra constraints. You model this with rdfs:subClassOf and your tools give you a nice tree.
The problem is that real resources don’t sit in one hierarchy. A sensor dataset might be a Dataset, a GovernedResource, and an EmbargoedAsset simultaneously. In a class hierarchy, you need multiple inheritance or mixin classes to express this. Those three classifications are orthogonal. Forcing them into a single tree creates a combinatorial problem that grows with every new axis.
Kurt Cagle wrote about this recently in The Ontologist: “skos:inScheme is a faceting relationship… compositions of orthogonal schemes [are] generally a better model than property inheritance for describing entities.”
I’d arrived at the same pattern from the opposite direction.
Flat membership, independent evaluation
In my stack, a resource declares its types as a flat set:
"@type": {Dataset: true, Governed: true, Embargoed: true}
No hierarchy. No parent class. Each type is a boolean membership. When I need to validate, each type independently determines which shapes apply. Dataset gets data-quality checks. Governed gets compliance checks. Embargoed gets access-policy checks. These three evaluations don’t know about each other.
In patent terms, each type is an independent claim. You can invalidate one without touching the others. If the types don’t narrow each other, forcing them into a hierarchy creates fragility for no reason.
This is exactly what skos:inScheme does. SKOS concepts aren’t classes. They’re labels in a concept scheme, which is why they work as facets: membership in one scheme says nothing about membership in another.
The equivalence wasn’t planned. I used flat sets because my constraint language doesn’t have class hierarchies. It was a limitation I worked around. Then I read Cagle’s article and realized the workaround was the correct semantic model.
Why this matters for SHACL
SHACL already supports this. You define shapes, and shapes apply to resources based on targets. The shapes don’t need to form a hierarchy. A resource can match multiple shapes, and each shape validates independently.
Same pattern. Flat membership. Independent evaluation.
The directionality issue disappears too. In a class hierarchy, you think top-down: kingdom, phylum, class, order. With skos:inScheme, there is no direction. A concept belongs to schemes. The schemes don’t need to relate to each other at all. No traversal, no transitivity, no “which direction do I walk the tree.” Flat lookup. O(1).
The practical version
If you’re modeling typed resources and reaching for rdfs:subClassOf, consider whether your types are really a hierarchy or just independent facets. If a resource can be a Dataset AND a Governed resource AND an Embargoed resource, and those three classifications don’t imply each other, you have facets, not a class tree.
Model them as facets. Validate them independently. Hierarchies still have their place for navigation and aggregation, but they shouldn’t be the default when your types are orthogonal. The SHACL shapes get simpler, the SKOS export gets cleaner, and you don’t spend time maintaining an inheritance graph that fights you every time a resource sits at the intersection of two branches.