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

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

Quick Start

Installation

dependencies:
  rdf_mapper: ^0.7.0

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.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),
  chapters: [
    Chapter('An Unexpected Party', 1),
    Chapter('Roast Mutton', 2),
    Chapter('A Short Rest', 3),
  ],
);

// 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: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}');
// --- 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;
  final Iterable<Chapter> chapters;

  Book({
    required this.id,
    required this.title,
    required this.author,
    required this.published,
    required this.isbn,
    required this.rating,
    required this.chapters,
  });
}

// 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 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)
        .addValues(chapterPredicate, book.chapters)
        .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),
      chapters: reader.getValues<Chapter>(chapterPredicate),
    );
  }
  
  // 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()
      .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));
  }
}

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