Getting Started with Kastor Gen
This tutorial will walk you through creating your first Kastor Gen application step by step.
💡 Why Kastor Gen? Generate type-safe domain interfaces automatically from SHACL/JSON-LD ontologies. 90% less manual code, 100% consistency with your ontology, compile-time type safety. See detailed benefits →
Prerequisites
- JDK 21+
- Kotlin 2.4 and KSP 2.3 (the Kastor build uses Kotlin 2.4.20 and KSP 2.3.12)
- Basic understanding of Kotlin
- Familiarity with RDF concepts (helpful but not required)
Fastest start: clone the Kastor repository and run
./gradlew :examples:hello-codegen:run. That module generates aPersoninterface and wrapper fromperson-shape.ttlwith the KSP processor and materializes RDF data with it../gradlew :examples:dcat-us:runGeneratedExampledoes the same for the DCAT-US 3.0 shapes.
Step 1: Project Setup
Kastor Gen artifacts are not yet published to a public repository. Use one of:
-
A module inside the Kastor build (what the examples do), with project dependencies:
plugins { id("org.jetbrains.kotlin.jvm") id("com.google.devtools.ksp") } dependencies { implementation(project(":kastor-gen:runtime")) ksp(project(":kastor-gen:processor")) // An RDF provider, e.g. for Rdf.memory() implementation(project(":rdf:jena")) // Optional: SHACL validation implementation(project(":kastor-gen:validation-jena")) } -
Your own project via Maven Local. In a Kastor checkout run
./gradlew publishToMavenLocalthen add
mavenLocal()to your repositories and use the published coordinates with the Kastor build’s project version:plugins { kotlin("jvm") version "2.4.20" id("com.google.devtools.ksp") version "2.3.12" } repositories { mavenLocal() mavenCentral() } dependencies { implementation("com.geoknoesis.kastor:kastor-gen-runtime:0.3.0-SNAPSHOT") ksp("com.geoknoesis.kastor:kastor-gen-processor:0.3.0-SNAPSHOT") }
Code generated from SHACL shapes (@Rdf(shacl = …)) carries Bean Validation annotations by default
(validationAnnotations = ValidationAnnotations.JAKARTA: @NotNull, @NotEmpty, @Size from sh:minCount /
sh:maxCount). kastor-gen-runtime provides jakarta.validation-api at compile time, so the default compiles
without another dependency (Gradle resolves it from the runtime’s module metadata; it is not added to your
runtime class path). With ValidationAnnotations.JAVAX add javax.validation:validation-api yourself, or use
ValidationAnnotations.NONE for no annotations. If the selected API is missing, the processor reports an error
at the annotation that names the dependency.
The processor needs no KSP arguments for hand-written @Rdf interfaces. When you generate from SHACL files
(@Rdf(shacl = …)), KSP cannot see edits to those files and the processor warns about each one it reads;
declare src/main/resources as an input of the kspKotlin task and set
kastor.gen.resources.tracked=true as shown in Incremental Builds.
All options are listed in the processor reference.
Step 2: Create Your First Domain Interface
Let’s create a simple Person interface:
// src/main/kotlin/com/example/Person.kt
package com.example
import com.geoknoesis.kastor.gen.annotations.Rdf
import com.geoknoesis.kastor.rdf.vocab.FOAF
@Rdf(iri = "http://xmlns.com/foaf/0.1/Person")
interface Person {
@Rdf(iri = "http://xmlns.com/foaf/0.1/name")
val name: List<String>
@Rdf(iri = "http://xmlns.com/foaf/0.1/age")
val age: List<Int>
@Rdf(iri = "http://xmlns.com/foaf/0.1/mbox")
val email: List<String>
}
Step 3: Build and Generate Code
Run the build to generate wrapper classes:
./gradlew build
This will generate wrapper classes in build/generated/ksp/main/kotlin/ that implement your domain interface and provide RDF side-channel access.
Step 4: Create RDF Data
Let’s create some sample RDF data:
// src/main/kotlin/com/example/Main.kt
package com.example
import com.geoknoesis.kastor.gen.runtime.*
import com.geoknoesis.kastor.rdf.*
import com.geoknoesis.kastor.rdf.vocab.FOAF
fun main() {
// Create an in-memory RDF repository
val repo = Rdf.memory()
// Add some sample data
repo.add {
val alice = iri("http://example.org/alice")
val bob = iri("http://example.org/bob")
// Alice's data
alice - FOAF.name - "Alice Johnson"
alice - FOAF.age - 30
alice - FOAF.mbox - "alice@example.com"
alice - FOAF.knows - bob
// Bob's data
bob - FOAF.name - "Bob Smith"
bob - FOAF.age - 25
bob - FOAF.mbox - "bob@example.com"
}
println("RDF data created successfully!")
}
Step 5: Materialize Domain Objects
Now let’s materialize RDF data into domain objects:
fun main() {
// ... (previous code)
// Materialize Alice as a Person object
val aliceRef = RdfRef(iri("http://example.org/alice"), repo.defaultGraph)
val alice: Person = aliceRef.asType()
// Use the domain interface
println("Name: ${alice.name.firstOrNull()}")
println("Age: ${alice.age.firstOrNull()}")
println("Email: ${alice.email.firstOrNull()}")
// Output:
// Name: Alice Johnson
// Age: 30
// Email: alice@example.com
}
Step 6: Access RDF Side-Channel
Let’s explore the RDF side-channel capabilities:
fun main() {
// ... (previous code)
// Access the RDF side-channel
val rdfHandle = alice.asRdf()
// Get the underlying RDF node and graph
println("Node: ${rdfHandle.node}")
println("Graph: ${rdfHandle.graph}")
// Access unmapped properties (extras)
val extras = rdfHandle.extras
val allPredicates = extras.predicates()
println("All predicates: ${allPredicates.map { it.value }}")
// Check if there are any unmapped properties
if (allPredicates.isNotEmpty()) {
println("Unmapped properties found:")
allPredicates.forEach { pred ->
val values = extras.values(pred)
println(" ${pred.value}: ${values.map { it.toString() }}")
}
}
}
Step 7: Add Validation (Optional)
If you added validation dependencies, you can enable SHACL validation:
fun main() {
// ... (previous code)
// Materialize with validation
val validation = JenaValidation.fromTurtle(shapesTtl) // shapesTtl: your SHACL shapes as Turtle text
val alice: Person = aliceRef.asValidatedType(validation)
println("Validation passed!")
// Or validate manually (using the same context)
val rdfHandle = alice.asRdf()
rdfHandle.validateOrThrow()
println("Manual validation passed!")
}
Step 8: Complete Example
Here’s the complete working example. In one file, the annotation com.geoknoesis.kastor.gen.annotations.Rdf and
the entry point com.geoknoesis.kastor.rdf.Rdf have the same simple name: import the annotation by name (an explicit
import wins over the com.geoknoesis.kastor.rdf.* star import) and the entry point under an alias.
package com.example
import com.geoknoesis.kastor.gen.annotations.Rdf
import com.geoknoesis.kastor.gen.runtime.*
import com.geoknoesis.kastor.rdf.*
import com.geoknoesis.kastor.rdf.Rdf as RdfApi
import com.geoknoesis.kastor.rdf.vocab.FOAF
@Rdf(iri = "http://xmlns.com/foaf/0.1/Person")
interface Person {
@Rdf(iri = "http://xmlns.com/foaf/0.1/name")
val name: List<String>
@Rdf(iri = "http://xmlns.com/foaf/0.1/age")
val age: List<Int>
@Rdf(iri = "http://xmlns.com/foaf/0.1/mbox")
val email: List<String>
}
fun main() {
// Create RDF repository and data
val repo = RdfApi.memory()
repo.add {
val alice = iri("http://example.org/alice")
alice - FOAF.name - "Alice Johnson"
alice - FOAF.age - 30
alice - FOAF.mbox - "alice@example.com"
}
// Materialize domain object
val aliceRef = RdfRef(iri("http://example.org/alice"), repo.defaultGraph)
val alice: Person = aliceRef.asType()
// Use domain interface
println("=== Domain Interface Usage ===")
println("Name: ${alice.name.firstOrNull()}")
println("Age: ${alice.age.firstOrNull()}")
println("Email: ${alice.email.firstOrNull()}")
// Access RDF side-channel
println("\n=== RDF Side-Channel Access ===")
val rdfHandle = alice.asRdf()
val extras = rdfHandle.extras
val predicates = extras.predicates()
if (predicates.isNotEmpty()) {
println("Unmapped properties:")
predicates.forEach { pred ->
val values = extras.values(pred)
println(" ${pred.value}: ${values.map { it.toString() }}")
}
} else {
println("No unmapped properties found")
}
repo.close()
}
What’s Next?
Congratulations! You’ve successfully created your first Kastor Gen application. Here’s what you’ve learned:
- ✅ How to set up Kastor Gen in a Kotlin project
- ✅ How to define domain interfaces with RDF annotations
- ✅ How to create and work with RDF data
- ✅ How to materialize RDF data into domain objects
- ✅ How to access RDF side-channels for advanced functionality
Next Steps
- Learn about Core Concepts - Understand the side-channel architecture
- Explore Domain Modeling - Learn best practices for domain interfaces
- Check out RDF Integration - Advanced RDF side-channel usage
- See Practical Examples - Real-world use cases
Troubleshooting
Common Issues
Q: Build fails with “No wrapper factory registered” A: Make sure KSP is properly configured and the build completed successfully. The wrapper classes need to be generated first.
Q: Annotations not found
A: Ensure you import com.geoknoesis.kastor.gen.annotations.Rdf (and Prefix when you declare prefix maps).
Q: Validation not working A: Check that you’ve added the validation dependencies and that a ValidationContext is registered.
Getting Help
- Check the FAQ for common questions
- Look at Examples for more patterns
- Review the API Reference for detailed documentation