RDF Mapper
Object-RDF mapping with zero boilerplate.
Bidirectional conversion between Dart objects and RDF graphs.
Getting Started
Install the packages
# Add runtime dependencies
dart pub add locorda_rdf_core locorda_rdf_mapper locorda_rdf_mapper_annotations
# Add Schema.org vocabulary constants (optional)
dart pub add locorda_rdf_terms_schema
# Add development dependencies (for code generation)
dart pub add build_runner locorda_rdf_mapper_generator --dev
# Generate the mappers
dart run build_runner build --delete-conflicting-outputs import 'package:locorda_rdf_mapper_annotations/annotations.dart';
import 'package:locorda_rdf_terms_schema/schema.dart';
// Import the generated init file (created by build_runner) - needed by main() below
import 'init_rdf_mapper.g.dart';
// Annotate your domain model
@RdfGlobalResource(
SchemaBook.classIri,
IriStrategy('{+baseUri}/books/{isbn}'),
)
class Book {
@RdfIriPart()
final String isbn;
@RdfProperty(SchemaBook.name)
final String title;
@RdfProperty(SchemaBook.author)
final String author;
@RdfProperty(SchemaBook.datePublished)
final DateTime published;
Book({
required this.isbn,
required this.title,
required this.author,
required this.published,
});
}
void main() {
// After running 'dart run build_runner build', use the generated mapper:
// Initialize generated mapper
final mapper = initRdfMapper(
baseUriProvider: () => 'https://example.org',
);
// Serialize to RDF
final book = Book(
isbn: '978-0-544-00341-5',
title: 'The Hobbit',
author: 'J.R.R. Tolkien',
published: DateTime(1937, 9, 21),
);
final turtle = mapper.encodeObject(book);
print(turtle);
// Deserialize from RDF
final deserializedBook = mapper.decodeObject<Book>(turtle);
print('Title: ${deserializedBook.title}');
} import 'package:locorda_rdf_mapper_annotations/annotations.dart';
import 'package:locorda_rdf_terms_schema/schema.dart';
// Import the generated init file (created by build_runner) - needed by main() below
import 'init_rdf_mapper.g.dart';
// Collections are automatically handled
@RdfGlobalResource(
SchemaBook.classIri,
IriStrategy('{+baseUri}/books/{isbn}'),
)
class Book {
@RdfIriPart()
final String isbn;
@RdfProperty(SchemaBook.name)
final String title;
// List - preserves order with RDF List structure
@RdfProperty(SchemaBook.hasPart, collection: rdfList)
final List<Chapter> chapters;
// Set - unordered collection (multiple triples)
@RdfProperty(SchemaBook.keywords)
final Set<String> keywords;
// Map - with custom entry class
@RdfProperty(SchemaBook.review)
@RdfMapEntry(ReviewEntry)
final Map<String, Review> reviews;
Book({
required this.isbn,
required this.title,
required this.chapters,
required this.keywords,
required this.reviews,
});
}
@RdfLocalResource(SchemaChapter.classIri)
class Chapter {
@RdfProperty(SchemaChapter.name)
final String title;
@RdfProperty(SchemaChapter.position)
final int number;
Chapter({required this.title, required this.number});
}
@RdfLocalResource(SchemaReview.classIri)
class ReviewEntry {
@RdfProperty(SchemaReview.author)
@RdfMapKey()
final String reviewer;
@RdfProperty(SchemaReview.reviewRating)
@RdfMapValue()
final Review rating;
ReviewEntry({required this.reviewer, required this.rating});
}
@RdfLocalResource(SchemaRating.classIri)
class Review {
@RdfProperty(SchemaRating.ratingValue)
final int stars;
Review({required this.stars});
}
void main() {
final book = Book(
isbn: '978-0-544-00341-5',
title: 'The Hobbit',
chapters: [
Chapter(title: 'An Unexpected Party', number: 1),
Chapter(title: 'Roast Mutton', number: 2),
],
keywords: {'fantasy', 'adventure', 'dragons'},
reviews: {
'Alice': Review(stars: 5),
'Bob': Review(stars: 4),
},
);
// After generation - initialize generated mapper:
final mapper = initRdfMapper(
baseUriProvider: () => 'https://example.org',
);
final turtle = mapper.encodeObject(book);
print(turtle);
} import 'package:locorda_rdf_core/core.dart';
import 'package:locorda_rdf_mapper_annotations/annotations.dart';
import 'package:locorda_rdf_terms_schema/schema.dart';
// Import the generated init file (created by build_runner) - needed by main() below
import 'init_rdf_mapper.g.dart';
// Lossless mapping with DECOUPLED unmapped triples
// Keeps domain model clean - unmapped triples stored separately
@RdfGlobalResource(
IriTerm('https://example.org/vocab/LosslessBook'),
IriStrategy('{+baseUri}/lossless-books/{isbn}'),
)
class BookLossless {
@RdfIriPart()
final String isbn;
@RdfProperty(SchemaBook.name)
final String title;
@RdfProperty(SchemaBook.author)
final String author;
BookLossless({
required this.isbn,
required this.title,
required this.author,
});
}
// Alternative: Use @RdfUnmappedTriples to store unmapped triples
// INSIDE domain objects (by default only captures triples about that specific object)
void main() {
// RDF data with unmapped properties + unrelated triples
const rdfData = '''
@prefix schema: <https://schema.org/> .
@prefix ex: <https://example.org/vocab/> .
<https://example.org/lossless-books/978-0-544-00341-5> a ex:LosslessBook ;
schema:name "The Hobbit" ;
schema:author "J.R.R. Tolkien" ;
schema:publisher "George Allen & Unwin" ;
schema:isbn "978-0-544-00341-5" ;
schema:numberOfPages "310" .
# Unrelated triples in the document
<https://example.org/publishers/allen-unwin> a schema:Organization ;
schema:name "George Allen & Unwin" ;
schema:location "London" .
''';
final mapper = initRdfMapper(
baseUriProvider: () => 'https://example.org',
);
// Decode with separate unmapped triples storage
final (book, unmappedTriples) =
mapper.decodeObjectLossless<BookLossless>(rdfData);
print('Book domain object is clean (no unmapped field)');
print(
'Unmapped triples stored separately: ${unmappedTriples.triples.length}');
// Includes: publisher, numberOfPages (book's) + ALL unrelated publisher triples
// Perfect round-trip - ALL triples preserved
final restoredRdf = mapper.encodeObjectLossless((book, unmappedTriples));
print('\nRestored RDF (everything preserved):\n$restoredRdf');
// Contains: book properties + unmapped book properties + unrelated triples!
} import 'package:locorda_rdf_core/core.dart';
import 'package:locorda_rdf_mapper_annotations/annotations.dart';
import 'package:locorda_rdf_terms_schema/schema.dart';
// Import the generated init file (created by build_runner) - needed by main() below
import 'init_rdf_mapper.g.dart';
// Complex IRI strategies with context variables
@RdfGlobalResource(
SchemaBook.classIri,
IriStrategy('{+baseUri}/books/{isbn}'),
)
class Book {
@RdfIriPart()
final String isbn;
@RdfProperty(SchemaBook.name)
final String title;
// Enum as IRI
@RdfProperty(SchemaBook.bookFormat)
final BookFormat format;
// Custom literal type with language tag
@RdfProperty(
SchemaBook.description,
literal: LiteralMapping.withLanguage('en'),
)
final String description;
Book({
required this.isbn,
required this.title,
required this.format,
required this.description,
});
}
// Enum mapping to IRIs
@RdfIri('https://schema.org/BookFormatType/{value}')
enum BookFormat {
@RdfEnumValue('Hardcover')
hardcover,
@RdfEnumValue('Paperback')
paperback,
@RdfEnumValue('EBook')
ebook,
}
// Custom literal type
@RdfLiteral()
class ISBN {
@RdfValue()
final String value;
ISBN(this.value);
// Custom serialization
LiteralTerm toRdfTerm() {
return LiteralTerm(value);
}
// Custom deserialization
static ISBN fromRdfTerm(LiteralTerm term) {
return ISBN(term.value);
}
}
void main() {
// After generation, initialize with context providers:
final mapper = initRdfMapper(
baseUriProvider: () => 'https://library.example.org',
);
final book = Book(
isbn: '978-0-544-00341-5',
title: 'The Hobbit',
format: BookFormat.hardcover,
description: 'A fantasy adventure novel',
);
final turtle = mapper.encodeObject(book);
print(turtle);
// IRI will be: https://library.example.org/books/978-0-544-00341-5
// format will be: <https://schema.org/BookFormatType/Hardcover>
// description will have @en language tag
} import 'package:locorda_rdf_core/core.dart';
import 'package:locorda_rdf_mapper/mapper.dart';
import 'package:locorda_rdf_terms_schema/schema.dart';
// For full control, implement mappers manually
class Book {
final String isbn;
final String title;
final String author;
Book({
required this.isbn,
required this.title,
required this.author,
});
}
// Manual mapper implementation
class BookMapper implements GlobalResourceMapper<Book> {
@override
IriTerm get typeIri => SchemaBook.classIri;
@override
Book fromRdfResource(IriTerm subject, DeserializationContext context) {
final reader = context.reader(subject);
return Book(
isbn: subject.value.split('/').last,
title: reader.require<String>(SchemaBook.name),
author: reader.require<String>(SchemaBook.author),
);
}
@override
(IriTerm, Iterable<Triple>) toRdfResource(
Book book,
SerializationContext context, {
RdfSubject? parentSubject,
}) {
final subject = IriTerm('https://example.org/books/${book.isbn}');
return context
.resourceBuilder(subject)
.addValue(SchemaBook.name, book.title)
.addValue(SchemaBook.author, book.author)
.build();
}
}
void main() {
// Create mapper and register custom implementation
final mapper = RdfMapper.withDefaultRegistry()
..registerMapper<Book>(BookMapper());
// Serialize
final book = Book(
isbn: '978-0-544-00341-5',
title: 'The Hobbit',
author: 'J.R.R. Tolkien',
);
final turtle = mapper.encodeObject(book);
print(turtle);
// Deserialize
final deserializedBook = mapper.decodeObject<Book>(turtle);
print('Title: ${deserializedBook.title}');
} What Can You Build?
ποΈ Domain Models β RDF
Map your Dart classes to RDF knowledge graphs. Define once with annotations, serialize to Turtle, JSON-LD and other formats automatically.
π Lossless RDF Pipelines
Build ETL pipelines that preserve all RDF data. Load, transform, and save without losing unmapped triples.
π¦ APIs with RDF Support
Create REST APIs that speak both JSON and RDF. Same domain model, multiple serialization formats.
Key Features
Zero Boilerplate
Annotate your classes, run build_runner, done. No
manual serialization code, no runtime reflection.
Bidirectional Mapping
Seamless conversion in both directions: Dart objects β RDF graphs. Type-safe deserialization from any RDF format.
Lossless Round-Trip
@RdfUnmappedTriples preserves unknown properties. Perfect
for ETL pipelines and evolving schemas.
Rich Collection Support
Lists, Sets, Maps with RDF Lists, Containers (Seq/Bag/Alt), and custom entry types.
Flexible IRI Strategies
URI templates, context variables, fragments, or custom mappers. Generate IRIs dynamically at runtime.
Type-Safe & Fast
Generated code with compile-time guarantees. No runtime overhead, full IDE support.
Yeah fine, but:
π "I need RDF/XML support"
β Works with mapper! Combine xml + mapper
Register the RDF/XML codec with a custom RdfCore,
then pass it to your mapper:
initRdfMapper(
baseUriProvider: () => 'https://example.org',
rdfMapper: RdfMapper(
registry: RdfMapperRegistry(),
rdfCore: RdfCore.withStandardCodecs(
additionalCodecs: [RdfXmlCodec()]
)
)
);
Then use contentType: 'application/rdf+xml' in your encodeObject/decodeObject
calls. Perfect for legacy format interop.
π·οΈ "I need custom property IRIs"
β Use terms-generator
Generate type-safe constants for your custom vocabularies. Feed it your ontology, get Dart classes out.
π "I need canonical form"
β Yes, canonicalization
RDF-CANON (RDFC-1.0) support for reliable graph comparison and cryptographic signing.
π§ "I need raw triple control"
β Use core directly
Work with triples directly when you need full control. Mapper is built on top of core.
The Mapper Family
RDF Mapper consists of three packages that work together:
locorda_rdf_mapper
Runtime system for mapping between Dart objects and RDF. Provides the base APIs and mapper interfaces.
locorda_rdf_mapper_annotations
Annotation system for declarative mapping. Annotate your classes to define RDF mappings.
locorda_rdf_mapper_generator
Code generator for zero-boilerplate mappers. Processes annotations and generates optimized mapping code.
Ready to Start?
Read the documentation or explore the API reference.
Troubleshooting
β οΈ Type checks fail / Mapper not found in tests
Your tests fail with "No deserializer found" or type checks fail even though everything looks correct.
Cause
Using relative imports (../lib/)
instead of package imports in tests creates different
Type objects. Dart treats import '../lib/book.dart'; and
import 'package:myapp/book.dart'; as completely different
types!
Fix
// β DON'T - Relative import
import '../lib/book.dart';
// β
DO - Package import
import 'package:myapp/book.dart'; Always use package imports in tests, examples, and anywhere the generated code might reference your classes.
π‘ Tip: Check the import statements in your generated
init_rdf_mapper.g.dart file β they use package imports,
so your code needs to use the same style.