Command line program and libraries for generating TypeScript code from SHACL shapes.
Working with RDF, SPARQL, and triple stores in code has never been as easy or productive as working with more mainstream technologies like relational databases. The RDF ecosystem lacks open source tools like dbt or Prisma that minimize friction in the developer experience and lower the barrier to adoption. The degree of friction is far out of proportion to any differences in essential complexity between the underlying graph and relational technologies.
SHACLmate is a tool for generating idiomatic, object-oriented abstractions over RDF graphs, so developers can concentrate more on concise business logic and less on the graph data model. Most tools in the RDF ecosystem take the opposite approach, emphasizing explicit, low-level manipulation and navigation of RDF graphs and execution of hand-written SPARQL queries at runtime (c.f., RDF JavaScript Libraries for JavaScript/TypeScript examples). That approach is comparable to writing SQL directly in code when working with a relational database. It's occasionally necessary, but it shouldn't be the only option, especially for developers who are relatively unfamiliar with the RDF data model.
SHACLmate currently generates idiomatic TypeScript classes/interfaces from SHACL shapes. Crafting SHACL shapes is an obvious barrier to adoption and a productive developer experience. There are few open source tools for working with SHACL, and the examples in this repository were hand-written in Turtle. We are working on support for other input formats (e.g., a hosted domain-specific language for defining shapes) as well as additional output formats (other programming languages).
- Node.js 18+
npx -y "@shaclmate/cli@latest" generate examples/kitchen-sink/src/kitchen-sink.shaclmate.ttl >generated.ts
Substituting the path to your SHACL file for the example path.
The generated code is serialized by ts-morph with minimal indentation and newlines. You will probably want to format it using a tool like Biome or prettier.
The generated TypeScript code relies on various third-party libraries such as sparql.js. To include these third-party libraries in your project, add @shaclmate/runtime
to your package.json
dependencies
.
Note that the @shaclmate/runtime
package does not include any code itself, only transitive dependencies
.
- Generated code works in browsers and Node.js
- TypeScript class or interface generation
- TypeScript union type generation from
sh:xone
- TypeScript literal type generation from
sh:in
- RDF serialization and deserialization function/method generation
- JSON serialization and deserialization function/method generation
- SPARQL CONSTRUCT query generation
- Deep equals function/method generation
- Deep hash function/method generation
- Instance identifier minting by deep hashing or UUID generation
- Zod schema generation. Zod schemas can be converted to JSON schemas using zod-to-json-schema.
- JSON Forms schema generation
- purify-ts
Maybe
types instead ofnull
/undefined
- Support for using handwritten "extern" types from generated code
- Built on RDF/JS standards
- Include/exclude generated code features at node shape or ontology level
- Decoupled concrete syntax -> abstract syntax -> code generator architecture (for future support of non-TypeScript code generators)
Kitchen sink (examples/kitchen-sink)
A "kitchen sink" demonstrating the code SHACLmate generates from many different SHACL shapes.
src/kitchen-sink.shaclmate.ttl
: SHACL shapes in RDF demonstrating different features of SHACLmatesrc/generated.ts
: generated TypeScript code
To reproduce the generated code to stdout, run:
npx -y "@shaclmate/cli@latest" generate examples/kitchen-sink/src/kitchen-sink.shaclmate.ttl
The compiler unit tests in packages/compiler/__tests__
use the kitchen sink examples to test generated code.
Forms (examples/forms)
This directory contains a web application with HTML forms rendered at runtime using JSON Forms schemas generated by SHACLmate.
Run the application with:
npm start
then open http://localhost:3000.
SHACLmate supports a subset of SHACL with a few extensions (under the shaclmate
namespace) necessary for generating code.
SHACLmate uses sh:name
to derive identifiers for generated code. Per the SHACL specification, sh:name
is only valid on property shapes. SHACLmate prefers shaclmate:name
for the identifiers of node shapes and as a way of overriding sh:name
on property shapes.
Only predicate paths are supported.
SHACLmate tries to resolve sh:class
to an rdfs:Class
/owl:Class
that is also an sh:NodeShape
.
SHACLmate generates built-in TypeScript types from corresponding XSD datatypes:
XSD datatype | TypeScript type |
---|---|
xsd:anyURI |
string |
xsd:boolean |
boolean |
xsd:date |
Date |
xsd:dateTime |
Date |
Numeric types | number |
xsd:string |
string |
All other datatypes generate an RDF/JS Literal
type
Every generated class or interface in TypeScript includes an identifier
property that (uniquely) identifies an instance of the class/interface.
On a node shape, sh:nodeKind
determines the type of identifier
: a blank node, a named node (IRI), or either.
On a property shape, sh:nodeKind
determines the type of the property.
SHACLmate uses sh:minCount
and sh:maxCount
on property shapes to generate containers for the underlying property shape type. Consider a property shape with sh:datatype
xsd:string
:
sh:minCount 1
andsh:maxCount 1
would generate a requiredstring
in TypeScriptsh:minCount 0
andsh:maxCount 0
would generate an option type e.g., apurify-ts
Maybe<string>
sh:minCount 1
withsh:maxCount
greater than 1 would generate a non-empty list type e.g., apurify-ts
NonEmptyList<string>
- All other combinations generate a possibly empty list e.g.,
string[]
.
Note that:
- A property shape without an
sh:minCount
has an implicitsh:minCount
of 0. sh:minCount
cannot be negative.sh:maxCount
must be greater than or equal tosh:minCount
.
sh:minCount
and sh:maxCount
are ignored on node shapes.
Recognized by the compiler but unsupported in generators.
sh:languageIn
is used to filter language-tagged literals when deserializing instances from RDF.
The other string-based constraint components are unsupported.
Unsupported.
sh:xone
is (mostly) supported on node shapes and property shapes and used to generate union types in TypeScript.sh:and
is recognized by the compiler but not generators.
sh:node
resolves a node shape as expected. Recursive node shapes are not supported in JSON (de)serialization but are supported by other features like RDF deserialization.
Qualified value shapes are unsupported.
sh:closed
and sh:ignoredProperties
are unsupported.
sh:hasValue
on a property shape generates code that checks for the expected value (among zero or more values of a property) on deserialization from RDF but not in other contexts.
sh:in
is used to generate literal types in TypeScript.
Unsupported.