Serializing Domain Interface Instances to RDF

Version: Kastor RDF SDK 0.3.0-SNAPSHOT · Kotlin 2.x · Java 21+

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:

  1. All triples where the resource is the subject (direct properties)
  2. 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 RdfBacked types (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.graph serializes 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 graph property contains all triples in the repository graph where the instance was materialized
  • It’s not limited to triples about the specific instance
  • The node property 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}")
}