Gradle Configuration for Ontology Generation
This tutorial generates domain interfaces, wrappers and a vocabulary from SHACL and JSON-LD context files
using only Gradle configuration — no @Rdf annotation in your sources. Every option is listed in the
Gradle Plugin Reference.
When to use the plugin instead of KSP
| Gradle plugin | KSP @Rdf(shacl = …) |
|
|---|---|---|
| Configuration | build script | annotation in a Kotlin file |
| Ontology file edits trigger regeneration | yes (task inputs) | only when you declare the files as kspKotlin inputs (Incremental Builds) |
| Vocabulary object generation | yes | no |
Data classes, NestedMode, write support |
no | yes |
Step 1: Apply the plugin
Kastor Gen is not yet published. Publish it to Maven Local from a Kastor checkout first:
./gradlew :kastor-gen:gradle-plugin:publishToMavenLocal :kastor-gen:runtime:publishToMavenLocal :rdf:core:publishToMavenLocal
// settings.gradle.kts
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}
// build.gradle.kts
plugins {
id("org.jetbrains.kotlin.jvm") version "2.4.20"
id("com.geoknoesis.kastor.gen") version "0.3.0-SNAPSHOT" // the Kastor build's project version
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation("com.geoknoesis.kastor:kastor-gen-runtime:0.3.0-SNAPSHOT")
// plus an RDF provider, e.g. com.geoknoesis.kastor:rdf-jena
}
Inside the Kastor build, apply id("com.geoknoesis.kastor.gen") and use project(":kastor-gen:runtime").
Step 2: Add the ontology files
project/
├── build.gradle.kts
└── src/main/resources/
└── ontologies/
├── dcat-us.shacl.ttl
└── dcat-us.context.jsonld
Paths may be relative to the project directory or to src/main/resources.
Step 3: Configure an ontology
kastorGen {
ontologies {
create("dcat") {
shaclPath = "ontologies/dcat-us.shacl.ttl"
contextPath = "ontologies/dcat-us.context.jsonld"
interfacePackage = "com.example.dcatus"
}
}
}
interfacePackage is the only required setting besides shaclPath. contextPath is optional; without a
context, type and property names come from IRI local names and sh:name. Wrappers and the vocabulary
default to the same package, and the DSL to com.example.dcatus.dsl.
The plugin needs org.jetbrains.kotlin.jvm or org.jetbrains.kotlin.multiplatform (with at least one JVM
target, e.g. jvm() or jvm("desktop")); Android projects are not supported.
Step 4: Build
./gradlew build
The plugin registers generateOntologyDcat (and the aggregate generateOntology) and adds its output
directory, build/generated/sources/kastor-gen/dcat, to the main Kotlin source set, so compileKotlin
runs generation automatically. You do not need to add sourceSets or dependsOn.
Run generation on its own with:
./gradlew generateOntologyDcat
Step 5: Use the generated code
import com.example.dcatus.Catalog
import com.geoknoesis.kastor.gen.runtime.materialize
import com.geoknoesis.kastor.rdf.*
val repo = Rdf.memory()
// ... load DCAT-US data ...
val catalog: Catalog = repo.defaultGraph.materialize(Iri("https://data.example.gov/catalog"))
println(catalog.title)
Separate packages
kastorGen {
ontologies {
create("dcat") {
shaclPath = "ontologies/dcat-us.shacl.ttl"
contextPath = "ontologies/dcat-us.context.jsonld"
interfacePackage = "com.example.dcatus.model"
wrapperPackage = "com.example.dcatus.rdf"
vocabularyPackage = "com.example.dcatus.vocab"
}
}
}
Wrappers are internal; application code uses the interfaces and materialize. When the wrapper package
differs, a small generated <Interface>Factory in the interface package keeps OntoMapper
auto-discovery working.
Vocabulary generation
Setting all three vocabulary properties enables vocabulary generation:
kastorGen {
ontologies {
create("dcat") {
shaclPath = "ontologies/dcat-us.shacl.ttl"
contextPath = "ontologies/dcat-us.context.jsonld"
interfacePackage = "com.example.dcatus"
vocabularyName = "DCAT"
vocabularyNamespace = "http://www.w3.org/ns/dcat#"
vocabularyPrefix = "dcat"
}
}
}
Result (abridged):
object DCAT : Vocabulary {
override val namespace: String = "http://www.w3.org/ns/dcat#"
override val prefix: String = "dcat"
val Catalog: Iri by lazy { term("Catalog") }
val Dataset: Iri by lazy { term("Dataset") }
val dataset: Iri by lazy { term("dataset") }
val title: Iri by lazy { Iri("http://purl.org/dc/terms/title") } // outside the dcat namespace
}
Terms outside the vocabulary namespace are written as full Iri("…") values. If two IRIs would get the
same name, the in-namespace term keeps it and the other is prefixed with its JSON-LD prefix
(dct_title).
For a vocabulary only:
create("dcatVocab") {
shaclPath = "ontologies/dcat-us.shacl.ttl"
contextPath = "ontologies/dcat-us.context.jsonld"
interfacePackage = "com.example.dcatus" // still required
generateInterfaces = false
generateWrappers = false
vocabularyName = "DCAT"
vocabularyNamespace = "http://www.w3.org/ns/dcat#"
vocabularyPrefix = "dcat"
}
Set generateVocabulary = false to switch it off even when the metadata is present.
DSL generation
create("dcat") {
shaclPath = "ontologies/dcat-us.shacl.ttl"
contextPath = "ontologies/dcat-us.context.jsonld"
interfacePackage = "com.example.dcatus"
generateDsl = true
dslName = "dcat" // optional
}
Without dslName the name is derived from the context file name and sanitised into an identifier
(dcat-us.context.jsonld → dcatUsContext). An explicit dslName must match [a-zA-Z][a-zA-Z0-9]*.
Multiple ontologies
kastorGen {
ontologies {
create("dcat") {
shaclPath = "ontologies/dcat-us.shacl.ttl"
contextPath = "ontologies/dcat-us.context.jsonld"
interfacePackage = "com.example.dcatus"
}
create("foaf") {
shaclPath = "ontologies/foaf.shacl.ttl"
contextPath = "ontologies/foaf.context.jsonld"
interfacePackage = "com.example.foaf"
}
}
}
Each ontology gets its own task and output directory; generateOntology runs all of them.
Conditional configuration
The configuration is ordinary Kotlin, so project properties can drive it:
val withDsl = providers.gradleProperty("kastor.dsl").isPresent
kastorGen {
ontologies {
create("dcat") {
shaclPath = "ontologies/dcat-us.shacl.ttl"
contextPath = "ontologies/dcat-us.context.jsonld"
interfacePackage = "com.example.dcatus"
generateDsl = withDsl
}
}
}
Incremental builds
The SHACL and context files are @InputFiles of the task and the task is @CacheableTask: editing either
file re-runs generation, unchanged inputs are up to date or restored from the build cache, and the new
files are written to a staging directory before they replace the previous run’s files (so a failed run
leaves the previous output intact). No manual inputs.files(...) or
outputs.cacheIf configuration is needed. See Incremental Builds.
Troubleshooting
| Message | Fix |
|---|---|
kastorGen ontology 'dcat': interfacePackage must be set |
Set interfacePackage. |
cannot read SHACL file … |
Check the path (project directory, then src/main/resources) and the Turtle syntax. |
generation reported errors: … / name collisions: … |
Two classes or properties map to the same Kotlin name; add distinct JSON-LD context terms or sh:name values. The message lists the IRIs. |
dslName '…' must match … |
Use letters and digits only, starting with a letter. |
vocabularyName is required when generateVocabulary is true |
Set all three vocabulary properties, or leave generateVocabulary unset. |
Unresolved reference in your code |
Check the package you import from; generation output is wired into main (or the main compilation of every JVM target in multiplatform projects) automatically. |
… applies neither org.jetbrains.kotlin.jvm nor org.jetbrains.kotlin.multiplatform … |
Apply one of the Kotlin plugins; Android is not supported. |
no JVM target found in … |
Add jvm() (or jvm("name")) to the multiplatform project. |
Inspect the tasks and resolved paths with:
./gradlew tasks --group=kastor-gen
./gradlew generateOntologyDcat --info
See also
- Gradle Plugin Reference
- Ontology Generation — the annotation (KSP) approach, type mapping and naming
- Domain Modeling
- FAQ