RDF Mapper for Dart

Seamless Bidirectional Mapping Between Dart Objects and RDF

Transform your Dart objects into powerful RDF graphs and back with ease. Like an ORM for semantic web data, RDF Mapper lets you work with linked data naturally.

pub package build codecov license
// Create a mapper with default registry
final rdfMapper = RdfMapper.withDefaultRegistry();

// Register our custom mapper
rdfMapper.registerMapper<Person>(PersonMapper());

// Create a person object
final person = Person(
  id: 'http://example.org/person/1',
  name: 'John Smith',
  age: 30,
);

// Serialize the person to RDF Turtle
final turtle = rdfMapper.encodeObject(person);

// Output will look like:
// <http://example.org/person/1> a foaf:Person ;
//   foaf:name "John Smith" ;
//   foaf:age 30 .

// Deserialize back to a Person object
final deserializedPerson = 
  rdfMapper.decodeObject<Person>(turtle);
// A simple domain model class
class Person {
  final String id;
  final String name;
  final int age;
  
  Person({
    required this.id, 
    required this.name, 
    required this.age
  });
  
  @override
  String toString() => 'Person(name: $name, age: $age)';
}
// Custom mapper for bidirectional conversion
class PersonMapper implements GlobalResourceMapper<Person> {
  // Define predicates as fields for readability
  final IriTerm namePredicate = IriTerm('http://xmlns.com/foaf/0.1/name');
  final IriTerm agePredicate = IriTerm('http://xmlns.com/foaf/0.1/age');
  
  // Define the RDF type for Person objects
  @override
  IriTerm? get typeIri => IriTerm('http://xmlns.com/foaf/0.1/Person');
  
  // Convert Person object to RDF (serialization)
  @override
  (IriTerm, List<Triple>) toRdfResource(
      Person value, 
      SerializationContext context,
      {RdfSubject? parentSubject}) {

    // Using fluent builder API for clean triple creation
    return context.resourceBuilder(IriTerm(value.id))
      .addValue(namePredicate, value.name)
      .addValue(agePredicate, value.age)
      .build();
  }
  
  // Convert RDF back to Person object (deserialization)
  @override
  Person fromRdfResource(IriTerm term, DeserializationContext context) {
    final reader = context.reader(term);
    
    return Person(
      id: term.iri,
      name: reader.require<String>(namePredicate),
      age: reader.require<int>(agePredicate),
    );
  }
}

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 Triple Diagram

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:

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 identifiers
    • LocalResourceMapper: For anonymous objects or auxiliary structures

Context Classes

  • SerializationContext: Provides access to the ResourceBuilder
  • DeserializationContext: Provides access to the ResourceReader

Fluent APIs

  • ResourceBuilder: For conveniently creating RDF resources
  • ResourceReader: For easily accessing RDF resource properties
RDF Mapper Architecture

Start Mapping Your Dart Objects to RDF Today

Empower your applications with the semantic web capabilities