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.