Serializing Domain Interface Instances to RDF
Version: Kastor RDF SDK
0.3.0-SNAPSHOT· Kotlin2.x· Java21+
Overview
Instances of interfaces generated by Kastor Gen implement RdfBacked, which provides access to the underlying RDF graph. This allows you to serialize domain objects directly to RDF formats like Turtle, JSON-LD, RDF/XML, and N-Triples.
⚠️ Important: The graph property contains the entire graph from which the instance was materialized, not just the triples about that specific instance. If you need to serialize only the instance-specific triples, see Serializing Only an Instance below.
Quick Start
import com.geoknoesis.kastor.rdf.RdfFormat
import com.geoknoesis.kastor.gen.runtime.asRdf
// Get a domain instance (e.g., from materialization)
val person: Person = // ... your domain instance
// Serialize the ENTIRE graph (contains all triples in the repository)
val fullGraphTurtle = person.rdf.graph.serialize(RdfFormat.TURTLE)
// Or use the convenience extension
val fullGraphTurtle2 = person.asRdf().graph.serialize(RdfFormat.TURTLE)
// To serialize ONLY this instance, create a filtered subgraph (see Example 5)
How It Works
RdfBacked Interface
All generated interface instances implement RdfBacked, which provides:
interface RdfBacked {
val rdf: RdfHandle
}
RdfHandle
The RdfHandle provides access to the underlying RDF graph:
interface RdfHandle {
val node: RdfTerm // The subject IRI or blank node (this instance)
val graph: RdfGraph // The ENTIRE RDF graph from the repository (not just this instance)
val extras: PropertyBag // Unmapped triples
}
Important: The graph property contains the complete repository graph, not just triples about the specific instance. To serialize only instance-specific triples, you must filter the graph (see Example 5).
Serialization
The RdfGraph has a serialize() extension method that supports multiple formats:
fun RdfGraph.serialize(
format: RdfFormat,
options: SerializationOptions = SerializationOptions.DEFAULT
): String
Examples
Example 1: Basic Serialization
import com.geoknoesis.kastor.rdf.*
import com.geoknoesis.kastor.rdf.vocab.FOAF
import com.geoknoesis.kastor.gen.runtime.asRdf
// Create a repository and add data
val repo = Rdf.memory()
repo.add {
val alice = iri("http://example.org/alice")
alice is FOAF.Person
alice has FOAF.name with "Alice Johnson"
alice has FOAF.email with "alice@example.org"
}
// Materialize to domain interface
val alice: Person = repo.defaultGraph
.getTriples()
.first { it.subject == Iri("http://example.org/alice") }
.let { RdfRef(it.subject as Iri, repo.defaultGraph) }
.asType<Person>()
// Serialize to Turtle
val turtle = alice.rdf.graph.serialize(RdfFormat.TURTLE)
println(turtle)
Output:
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
<http://example.org/alice> a foaf:Person ;
foaf:name "Alice Johnson" ;
foaf:email "alice@example.org" .
Example 2: Serialize to JSON-LD
val jsonld = person.rdf.graph.serialize(RdfFormat.JSON_LD) {
prettyPrint = true
jsonLdContext = mapOf(
"@context" to mapOf(
"foaf" to "http://xmlns.com/foaf/0.1/",
"name" to "foaf:name",
"email" to "foaf:email"
)
)
}
println(jsonld)
Example 3: Serialize with Custom Options
val turtle = person.rdf.graph.serialize(RdfFormat.TURTLE) {
prettyPrint = true
baseUri = "http://example.org/"
prefix("ex", "http://example.org/")
prefix("foaf", "http://xmlns.com/foaf/0.1/")
lineWidth = 120
}
Example 4: Serialize Multiple Instances
When you have multiple domain instances, they all share the same underlying graph:
val repo = Rdf.memory()
repo.add {
val alice = iri("http://example.org/alice")
val bob = iri("http://example.org/bob")
alice is FOAF.Person
alice has FOAF.name with "Alice"
alice has FOAF.knows with bob
bob is FOAF.Person
bob has FOAF.name with "Bob"
}
// Materialize both
val alice: Person = // ... materialize alice
val bob: Person = // ... materialize bob
// Serialize - both instances share the same graph
// This will serialize the ENTIRE graph including both Alice and Bob
val turtle = alice.rdf.graph.serialize(RdfFormat.TURTLE)
// Same result as: bob.rdf.graph.serialize(RdfFormat.TURTLE)
Note: Both alice.rdf.graph and bob.rdf.graph reference the same graph object, so serializing either one produces the same output containing all triples in the repository.
Example 5: Serializing Only an Instance
Important: By default, person.rdf.graph contains the entire repository graph, not just triples about that person. If you want to serialize only the triples related to a specific instance, you need to create a filtered subgraph:
import com.geoknoesis.kastor.rdf.Rdf
// Get all triples where the instance is the subject
val instanceTriples = person.rdf.graph.getTriples()
.filter { it.subject == person.rdf.node }
// Create a new graph with only those triples
val instanceGraph = Rdf.graph {
instanceTriples.forEach { triple ->
add(triple)
}
}
// Serialize only the instance-specific triples
val turtle = instanceGraph.serialize(RdfFormat.TURTLE)
Alternative: Include related objects
If you also want to include triples where the instance appears as an object (e.g., when someone else references this person):
// Get triples where instance is subject OR object
val allInstanceTriples = person.rdf.graph.getTriples()
.filter {
it.subject == person.rdf.node ||
it.obj == person.rdf.node
}
val instanceGraph = Rdf.graph {
allInstanceTriples.forEach { triple ->
add(triple)
}
}
val turtle = instanceGraph.serialize(RdfFormat.TURTLE)
When to use each approach:
- Full graph (
person.rdf.graph.serialize()): Use when you want to export the entire dataset/repository - Instance-only graph: Use when you want to export just one resource and its direct properties
- Instance + references: Use when you want the instance plus any resources that reference it
Example 6: Using writeToGraph() with CBD Closure
Recommended: The writeToGraph() method provides a convenient way to write the CBD (Concise Bounded Description) closure of an instance to a target graph. CBD includes all direct properties and recursively follows blank nodes to get complete anonymous resource descriptions.
import com.geoknoesis.kastor.rdf.Rdf
import com.geoknoesis.kastor.gen.runtime.writeToGraph
val person: Person = // ... your instance
// Create a new graph
val targetGraph = Rdf.graph()
// Write CBD closure to target graph (uses rdf.node as subject)
person.writeToGraph(targetGraph)
// Serialize the CBD closure
val turtle = targetGraph.serialize(RdfFormat.TURTLE)
CBD Closure Definition:
CBD (Concise Bounded Description) includes:
- All triples where the resource is the subject (direct properties)
- Recursively, for any blank node object, all triples where that blank node is the subject
This means:
- Direct properties of the instance are included
- Blank node objects are followed recursively (their properties are included)
- IRI objects are not followed (they remain as references)
Example with Blank Nodes:
// Graph structure:
// :alice foaf:name "Alice"
// :alice foaf:knows _:b1
// _:b1 foaf:name "Bob"
// _:b1 foaf:email "bob@example.com"
// _:b1 foaf:knows _:b2
// _:b2 foaf:name "Charlie"
val person: Person = // ... alice instance
val targetGraph = Rdf.graph()
person.writeToGraph(targetGraph)
// targetGraph now contains all 6 triples:
// - :alice foaf:name "Alice"
// - :alice foaf:knows _:b1
// - _:b1 foaf:name "Bob"
// - _:b1 foaf:email "bob@example.com"
// - _:b1 foaf:knows _:b2
// - _:b2 foaf:name "Charlie"
Writing to a Different Subject:
// Write CBD closure with a different subject IRI
val targetGraph = Rdf.graph()
person.writeToGraph(targetGraph, subject = Iri("http://example.org/copy"))
// The CBD closure is written with the new subject
// (Note: blank node IDs may be remapped)
Benefits of writeToGraph():
- ✅ Automatic CBD extraction: No manual filtering needed
- ✅ Recursive blank node handling: Complete anonymous resource descriptions
- ✅ Type-safe: Works with all generated domain interfaces
- ✅ RDFBeans-like behavior: Similar to RDFBeans serialization without reflection
- ✅ Flexible: Can write to any mutable graph with optional subject override
When to use writeToGraph():
- ✅ When you need the complete resource description (including blank node properties)
- ✅ When exporting a single resource with all its nested anonymous structures
- ✅ When copying an instance to a different graph
- ✅ When you want RDFBeans-like bidirectional conversion (domain object ↔ RDF)
When to use manual filtering (Example 5):
- When you need only direct properties (no blank node recursion)
- When you need custom filtering logic
- When you need to include incoming references (triples where instance is object)
Using Convenience Extensions
asRdf() Extension
For convenience, you can use the asRdf() extension function:
import com.geoknoesis.kastor.gen.runtime.asRdf
val person: Person = // ... your instance
// These are equivalent:
val turtle1 = person.rdf.graph.serialize(RdfFormat.TURTLE)
val turtle2 = person.asRdf().graph.serialize(RdfFormat.TURTLE)
writeToGraph() Extension
The writeToGraph() extension method provides a convenient way to write the CBD closure of an instance:
import com.geoknoesis.kastor.gen.runtime.writeToGraph
val person: Person = // ... your instance
val targetGraph = Rdf.graph()
// Write CBD closure (uses rdf.node as subject)
person.writeToGraph(targetGraph)
// Or specify a different subject
person.writeToGraph(targetGraph, subject = Iri("http://example.org/copy"))
Note: The writeToGraph() method is available both as:
- An extension function on
RdfBackedtypes (recommended) - A generated method on wrapper classes (for direct wrapper access)
Supported Formats
All standard RDF formats are supported:
- Turtle (
RdfFormat.TURTLE) - Human-readable RDF syntax - JSON-LD (
RdfFormat.JSON_LD) - JSON-based RDF serialization - RDF/XML (
RdfFormat.RDF_XML) - XML-based RDF serialization - N-Triples (
RdfFormat.N_TRIPLES) - Simple line-based RDF syntax
Serialization Options
You can customize serialization using SerializationOptions:
val turtle = person.rdf.graph.serialize(RdfFormat.TURTLE) {
prettyPrint = true // Enable pretty printing
baseUri = "http://example.org/" // Base URI for relative IRIs
prefix("ex", "http://example.org/") // Custom prefix mappings
lineWidth = 120 // Maximum line width
}
For JSON-LD specific options:
val jsonld = person.rdf.graph.serialize(RdfFormat.JSON_LD) {
jsonLdContext = mapOf(/* ... */) // JSON-LD context
jsonLdCompact = true // Enable compaction
jsonLdFrame = mapOf(/* ... */) // JSON-LD frame
}
See Serialization Options Guide for complete details.
Important Notes
Graph Sharing
- Multiple instances from the same repository share the same underlying graph
- Serializing
instance.rdf.graphserializes the ENTIRE repository graph, not just the instance - All instances from the same repository will serialize to the same output (the full graph)
- Use subgraph filtering (see Example 5) if you need instance-specific serialization
Graph vs Instance
- The
graphproperty contains all triples in the repository graph where the instance was materialized - It’s not limited to triples about the specific instance
- The
nodeproperty identifies the instance’s subject IRI - To serialize only instance-specific triples, you must create a filtered subgraph
When to Use Full Graph vs Instance-Only
Use full graph serialization when:
- Exporting an entire dataset or repository
- Sharing complete RDF data
- Backing up or archiving all data
- Interoperating with other systems that need the full context
Use instance-only serialization when:
- Exporting a single resource for sharing
- Creating resource-specific API responses
- Reducing serialization size for specific resources
- Isolating a resource from its context
Performance
- Serialization is efficient and uses the underlying provider (Jena/RDF4J)
- Large graphs may take time to serialize
- Consider filtering if you only need a subset of triples
Error Handling
Serialization may throw RdfFormatException if:
- The requested format is not supported by any provider
- Serialization fails due to invalid data
- Provider-specific errors occur
try {
val turtle = person.rdf.graph.serialize(RdfFormat.TURTLE)
} catch (e: RdfFormatException.UnsupportedFormat) {
println("Format not supported: ${e.formatName}")
} catch (e: RdfFormatException) {
println("Serialization failed: ${e.message}")
}
Related Documentation
- How to Serialize RDF - General RDF serialization guide
- RdfBacked Interface - Runtime API documentation
- CBD Closure - Understanding Concise Bounded Description
- Materialization - Creating domain instances from RDF
- Core API - RDF core API including CBD closure