Key Features
Bidirectional Mapping
Seamless conversion between Dart objects and RDF representations
Type-Safe API
Fully typed API for safe RDF mapping operations
Extensible
Easy creation of custom mappers for domain-specific types
Flexible
Support for all core RDF concepts: IRI nodes, blank nodes, and literals
Dual API
Work with RDF strings or directly with graph structures
Compatible
Built on top of rdf_core
for seamless integration
RDF Collections
Multiple approaches: RDF Lists (ordered), Multi-Objects (flat), and RDF Containers (Seq/Bag/Alt) for every collection need
RDF Containers
Complete support for RDF containers: Seq (ordered), Bag (unordered), Alt (alternatives with preference)
Lossless Mapping
Preserve unmapped RDF data with getUnmapped
/addUnmapped
APIs
What is RDF?
Resource Description Framework (RDF) is a standard model for data interchange on the Web. It extends the linking structure of the Web by using URIs to name relationships between things as well as the two ends of the link.
RDF is built around statements known as "triples" in the form of subject-predicate-object:
- Subject: The resource being described (identified by an IRI or blank node)
- Predicate: The property or relationship (always an IRI)
- Object: The value or related resource (an IRI, blank node, or literal value)
RDF Collections: Two Main Approaches for Different Needs
RDF Lists (Ordered)
Use addRdfList()
and optionalRdfList()
when order matters:
- Book chapters (1, 2, 3...)
- Process steps
- Navigation breadcrumbs
- Recipe instructions
# RDF List (ordered, structured)
<book> schema:hasPart _:list1 .
_:list1 rdf:first <chapter1> ; rdf:rest _:list2 .
_:list2 rdf:first <chapter2> ; rdf:rest rdf:nil .
Multi-Objects (Unordered)
Use addValues()
/getValues()
or custom MultiObjectsSerializer
for flat multiple values:
- Tags and categories
- Multiple authors
- Contact emails
- Supported formats
- Custom collection types
# Multi-Objects (flat, unordered)
<book> schema:genre "fantasy", "adventure", "children" ;
schema:author <author1>, <author2> .
# Same result with MultiObjectsSerializer:
<library> lib:featuredBooks <book1>, <book2>, <book3> .
Note: Both addValues()
/getValues()
and custom MultiObjectsSerializer
/MultiObjectsDeserializer
produce the same RDF structure - multiple triples with the same predicate. The choice depends on whether you need custom collection types or can use the built-in convenience methods.
RDF Containers: Seq, Bag, Alt
Beyond RDF Lists, the library provides full support for the three standard RDF container types using numbered properties (rdf:_1
, rdf:_2
, etc.):
rdf:Seq (Sequence)
Use addRdfSeq()
and optionalRdfSeq()
for ordered sequences where position has semantic meaning:
- Tutorial chapters and lessons
- Process steps and workflows
- Rankings and priority lists
- Sequential data processing
# rdf:Seq (ordered sequence)
<tutorial> ex:hasChapter _:seq1 .
_:seq1 a rdf:Seq ;
rdf:_1 "Introduction" ;
rdf:_2 "Getting Started" ;
rdf:_3 "Advanced Topics" .
rdf:Bag (Unordered Collection)
Use addRdfBag()
and optionalRdfBag()
for unordered collections where order doesn't matter semantically:
- Keywords and tags
- Categories and classifications
- Learning objectives
- Document attributes
# rdf:Bag (unordered collection)
<tutorial> ex:hasKeyword _:bag1 .
_:bag1 a rdf:Bag ;
rdf:_1 "semantic-web" ;
rdf:_2 "tutorial" ;
rdf:_3 "programming" .
rdf:Alt (Alternatives)
Use addRdfAlt()
and optionalRdfAlt()
for alternative values with preference ordering:
- Media formats (preferred → fallback)
- Language versions
- Alternative representations
- Backup options
# rdf:Alt (alternatives with preference)
<video> ex:hasFormat _:alt1 .
_:alt1 a rdf:Alt ;
rdf:_1 "video/webm" ; # Most preferred
rdf:_2 "video/mp4" ; # Fallback
rdf:_3 "video/avi" . # Last resort
Container vs List Comparison
Feature | RDF Lists | RDF Containers |
---|---|---|
Structure | Linked list (rdf:first/rdf:rest) | Numbered properties (rdf:_1, rdf:_2) |
Standards | RDF 1.1 Lists | RDF 1.0 Containers |
Container Types | One type (ordered) | Three types (Seq/Bag/Alt) |
Use Cases | Simple ordered collections | Typed collections with specific semantics |
Quick Start
Installation
dependencies:
rdf_mapper: ^0.10.1
Or use the following command:
dart pub add rdf_mapper
Basic Setup
import 'package:rdf_mapper/rdf_mapper.dart';
// Create a mapper instance with default registry
final rdfMapper = RdfMapper.withDefaultRegistry();
Usage Examples
import 'package:rdf_core/rdf_core.dart';
import 'package:rdf_mapper/rdf_mapper.dart';
import 'package:rdf_vocabularies_schema/schema.dart';
// Create mapper instance with default registry
final rdf = RdfMapper.withDefaultRegistry()
// Register our custom mappers
..registerMapper<Book>(BookMapper())
..registerMapper<Chapter>(ChapterMapper())
..registerMapper<ISBN>(ISBNMapper())
..registerMapper<Rating>(RatingMapper());
// Create a book with chapters
final book = Book(
id: 'hobbit', // Just the identifier, not the full IRI
title: 'The Hobbit',
author: 'J.R.R. Tolkien',
published: DateTime(1937, 9, 21),
isbn: ISBN('9780618260300'),
rating: Rating(5),
// ORDERED: Chapters must be in sequence (preserves element order)
chapters: [
Chapter('An Unexpected Party', 1),
Chapter('Roast Mutton', 2),
Chapter('A Short Rest', 3),
],
// UNORDERED: Multiple independent values (no guaranteed order)
genres: ['fantasy', 'adventure', 'children'],
);
// Convert the book to RDF Turtle format
final turtle = rdf.encodeObject(book);
// Result will be nicely formatted RDF Turtle
// RDF Turtle input to deserialize
final turtleInput = '''
@base <http://example.org/book/> .
@prefix schema: <https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<hobbit> a schema:Book;
schema:aggregateRating 5;
schema:author "J.R.R. Tolkien";
schema:datePublished "1937-09-20T23:00:00.000Z"^^xsd:dateTime;
schema:genre "adventure", "children", "fantasy";
schema:hasPart ([ a schema:Chapter ; schema:name "An Unexpected Party" ; schema:position 1 ] [ a schema:Chapter ; schema:name "Roast Mutton" ; schema:position 2 ] [ a schema:Chapter ; schema:name "A Short Rest" ; schema:position 3 ]);
schema:isbn <urn:isbn:9780618260300>;
schema:name "The Hobbit" .
''';
// Deserialize RDF back to our Book object with all its chapters
final book = rdf.decodeObject<Book>(turtleInput);
// Access complex properties
print('Title: ${book.title}');
print('Author: ${book.author}');
print('Published: ${book.published}');
print('Rating: ${book.rating.stars} stars');
print('Chapters: ${book.chapters.length}');
// Lossless mapping preserves unmapped RDF data
// that couldn't be processed by your mappers
// Deserialize with lossless APIs
final (book, unmappedData) = rdf.decodeObjectLossless<Book>(turtleInput);
// Access the original mapped object
print('Book: ${book.title} by ${book.author}');
// Check what RDF data wasn't mapped to Dart properties
if (unmappedData.isNotEmpty) {
print('Found ${unmappedData.length} unmapped triples');
// You could store this data, log it, or process it later
for (final triple in unmappedData) {
print('Unmapped: ${triple.subject} ${triple.predicate} ${triple.object}');
}
}
// When serializing back, preserve the unmapped data
final modifiedBook = Book(
id: book.id,
title: 'The Hobbit (Annotated Edition)', // Modified title
author: book.author,
published: book.published,
isbn: book.isbn,
rating: book.rating,
chapters: book.chapters,
);
// Add unmapped data back during serialization
final newTurtle = rdf.encodeObjectLossless(modifiedBook, unmappedData);
// The result contains both your object changes AND the original unmapped RDF
// --- Domain Model ---
// Primary entity with an identifier
class Book {
final String id;
final String title;
final String author;
final DateTime published;
final ISBN isbn;
final Rating rating;
// ORDERED: Chapters sequence matters (mapped to RDF list)
final List<Chapter> chapters;
// UNORDERED: Multiple independent values (mapped to multiple triples)
final List<String> genres;
Book({
required this.id,
required this.title,
required this.author,
required this.published,
required this.isbn,
required this.rating,
required this.chapters,
required this.genres,
});
}
// Value object using blank nodes (no identifier)
class Chapter {
final String title;
final int number;
Chapter(this.title, this.number);
}
// Custom identifier type using IRI mapping
class ISBN {
final String value;
ISBN(this.value);
}
// Custom value type using literal mapping
class Rating {
final int stars;
Rating(this.stars) {
if (stars < 0 || stars > 5) {
throw ArgumentError('Rating must be between 0 and 5 stars');
}
}
}
// Book mapper - maps Book objects to IRI nodes
class BookMapper implements GlobalResourceMapper<Book> {
// Property predicates for semantic clarity
static final titlePredicate = SchemaBook.name;
static final authorPredicate = SchemaBook.author;
static final publishedPredicate = SchemaBook.datePublished;
static final isbnPredicate = SchemaBook.isbn;
static final ratingPredicate = SchemaBook.aggregateRating;
static final chapterPredicate = SchemaBook.hasPart;
static final genrePredicate = SchemaBook.genre;
static const String bookIriPrefix = 'http://example.org/book/';
@override
final IriTerm typeIri = SchemaBook.classIri;
@override
(IriTerm, List<Triple>) toRdfResource(
Book book,
SerializationContext context, {
RdfSubject? parentSubject,
}) {
return context
.resourceBuilder(IriTerm('$bookIriPrefix${book.id}'))
.addValue(titlePredicate, book.title)
.addValue(authorPredicate, book.author)
.addValue<DateTime>(publishedPredicate, book.published)
.addValue<ISBN>(isbnPredicate, book.isbn)
.addValue<Rating>(ratingPredicate, book.rating)
// ORDERED: Use addRdfList for chapters (preserves sequence)
.addRdfList<Chapter>(chapterPredicate, book.chapters)
// UNORDERED: Use addValues for genres (multiple independent values)
.addValues<String>(genrePredicate, book.genres)
.build();
}
@override
Book fromRdfResource(IriTerm term, DeserializationContext context) {
final reader = context.reader(term);
return Book(
id: _extractIdFromIri(term.iri),
title: reader.require<String>(titlePredicate),
author: reader.require<String>(authorPredicate),
published: reader.require<DateTime>(publishedPredicate),
isbn: reader.require<ISBN>(isbnPredicate),
rating: reader.require<Rating>(ratingPredicate),
// ORDERED: Use optionalRdfList for chapters (preserves sequence)
chapters: reader.optionalRdfList<Chapter>(chapterPredicate) ?? const [],
// UNORDERED: Use getValues for genres (multiple independent values)
genres: reader.getValues<String>(genrePredicate).toList(),
);
}
// Helper method to extract ID from IRI
String _extractIdFromIri(String iri) {
if (!iri.startsWith(bookIriPrefix)) {
throw ArgumentError('Invalid Book IRI format: $iri');
}
return iri.substring(bookIriPrefix.length);
}
}
// Chapter mapper - maps Chapters to blank nodes
class ChapterMapper implements LocalResourceMapper<Chapter> {
static final titlePredicate = SchemaChapter.name;
static final numberPredicate = SchemaChapter.position;
@override
final IriTerm typeIri = SchemaChapter.classIri;
@override
(BlankNodeTerm, List<Triple>) toRdfResource(
Chapter chapter,
SerializationContext context, {
RdfSubject? parentSubject,
}) {
return context.resourceBuilder(BlankNodeTerm())
.addValue(titlePredicate, chapter.title)
.addValue(numberPredicate, chapter.number)
.build();
}
@override
Chapter fromRdfResource(BlankNodeTerm term, DeserializationContext context) {
final reader = context.reader(term);
return Chapter(
reader.require<String>(titlePredicate),
reader.require<int>(numberPredicate),
);
}
}
// ISBN mapper - maps ISBN to IRI term
class ISBNMapper implements IriTermMapper<ISBN> {
static const String ISBN_URI_PREFIX = 'urn:isbn:';
@override
IriTerm toRdfTerm(ISBN value, SerializationContext context) {
return IriTerm('$ISBN_URI_PREFIX${value.value}');
}
@override
ISBN fromRdfTerm(IriTerm term, DeserializationContext context) {
final uri = term.iri;
if (!uri.startsWith(ISBN_URI_PREFIX)) {
throw ArgumentError('Invalid ISBN URI format: $uri');
}
return ISBN(uri.substring(ISBN_URI_PREFIX.length));
}
}
// Rating mapper - maps Rating to literal term
class RatingMapper implements LiteralTermMapper<Rating> {
@override
LiteralTerm toRdfTerm(Rating value, SerializationContext context) {
return LiteralTerm.typed(value.stars.toString(), 'integer');
}
@override
Rating fromRdfTerm(LiteralTerm term, DeserializationContext context) {
return Rating(int.parse(term.value));
}
}
Additional Examples
Explore more comprehensive examples in the GitHub repository:
- RDF Containers - rdf_containers_example.dart: Complete demonstration of rdf:Seq, rdf:Bag, and rdf:Alt containers
- RDF Collections - collections_example.dart: Ordered vs unordered collections with practical scenarios
- Enum Mapping - enum_mapping_example.dart: Both literal and IRI-based enum mapping approaches
- Custom Collections - custom_collections_example.dart: Advanced collection types and custom deserializers
Architecture
Mapper Hierarchy
-
Term Mappers: For simple values
IriTermMapper
: For IRIs (e.g., URIs, URLs)LiteralTermMapper
: For literal values (strings, numbers, dates)
-
Resource Mappers: For complex objects with multiple properties
GlobalResourceMapper
: For objects with globally unique identifiersLocalResourceMapper
: For anonymous objects or auxiliary structures
Context Classes
SerializationContext
: Provides access to the ResourceBuilderDeserializationContext
: Provides access to the ResourceReader
Fluent APIs
ResourceBuilder
: For conveniently creating RDF resourcesResourceReader
: For easily accessing RDF resource properties
Start Mapping Your Dart Objects to RDF Today
Empower your applications with the semantic web capabilities