Lutaml::Model provides a unified rdf DSL that defines RDF mappings once for both JSON-LD and Turtle serialization. This follows the same principle as key_value do …​ end which serves JSON, YAML, and TOML from a single block.

Both JSON-LD and Turtle are RDF serialization formats representing the same subject–predicate–object triples. The rdf DSL lets you define the mapping once, and the format adapters handle syntax differences automatically.

Setup

Add the rdf-turtle gem to your Gemfile (required for Turtle output):

gem "rdf-turtle", "~> 3.3"

No additional gem is needed for JSON-LD output.

When to Use rdf vs Separate jsonld/turtle Blocks

Use rdf do …​ end when: * Your model maps to RDF resources using standard vocabularies (SKOS, DC, etc.) * You need both JSON-LD and Turtle output from the same model * You want predicate-based mapping with automatic @context generation

Use jsonld do …​ end when: * You need full control over the JSON-LD @context structure * You are mapping to JSON properties that don’t map to RDF predicates

Use turtle do …​ end when: * You only need Turtle output * You need Turtle-specific features not available in the unified DSL

Both the unified rdf block and the format-specific blocks can coexist on the same model. If both are defined, format-specific blocks take precedence.

The rdf DSL

Basic Model

class Concept < Lutaml::Model::Serializable
  attribute :code, :string
  attribute :name, :string

  rdf do
    namespace SkosNamespace

    subject { |m| "http://example.org/concept/#{m.code}" }
    type "skos:Concept"

    predicate :notation,  namespace: SkosNamespace, to: :code
    predicate :prefLabel, namespace: SkosNamespace, to: :name
  end
end

This single rdf block creates mappings for both :turtle and :jsonld formats automatically.

DSL Methods

namespace

Declares the RDF namespaces used by predicates. Accepts one or more Lutaml::Rdf::Namespace subclass references:

namespace SkosNamespace, DctermsNamespace

In Turtle output, each namespace becomes an @prefix declaration. In JSON-LD output, each becomes a prefix entry in @context.

subject

Required for top-level resources. A block that generates the subject URI from the model instance:

subject { |m| "http://example.org/concept/#{m.code}" }

type

Sets the RDF type (rdf:type). Accepts a single compact IRI or an array of compact IRIs. Compact IRIs are resolved via declared namespaces:

type "skos:Concept"
# resolves to <http://www.w3.org/2004/02/skos/core#Concept>

# multiple types:
type ["skos:Concept", "dcterms:Agent"]

In Turtle, each type produces a separate a triple. In JSON-LD, a single type produces "@type": "skos:Concept" while multiple types produce an array "@type": ["skos:Concept", "dcterms:Agent"].

predicate

Each predicate creates a mapping between an RDF predicate and a model attribute:

predicate :prefLabel, namespace: SkosNamespace, to: :name, lang_tagged: true

Parameters:

  • name — the local name in the namespace (e.g., :prefLabel)

  • namespace: — the Lutaml::Rdf::Namespace subclass (required)

  • to: — the model attribute to read (required)

  • lang_tagged: (default: false) — if true, values are serialized with language tags (see Language-Tagged Values). Mutually exclusive with uri_reference.

  • uri_reference: (default: false) — if true, values are serialized as URI references rather than string literals (see URI Reference Predicates). Mutually exclusive with lang_tagged.

members

Declares that a container model contains member resources that should be serialized as separate subjects in the output graph (see Graph Serialization).

Optional linking predicate parameters generate relationship triples from the container to each member:

members :concepts, predicate_name: :member, namespace: SkosNamespace

When predicate_name and namespace are provided:

  • Turtle: produces skos:member <child-uri> triples on the container subject

  • JSON-LD: adds a member term to @context with "@type": "@id" and {"@id": "…​"} references in the container resource

Parameters:

  • attr_name — the model attribute holding the member collection (required)

  • predicate_name: — the local name for the linking predicate (optional)

  • namespace: — the Lutaml::Rdf::Namespace subclass for the linking predicate (required when predicate_name is given)

Serialization

Turtle

concept = Concept.new(code: "2119", name: "component")
turtle = concept.to_turtle

Produces:

@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

<http://example.org/concept/2119> a skos:Concept;
  skos:notation "2119";
  skos:prefLabel "component" .

JSON-LD

jsonld = concept.to_jsonld

Produces:

{
  "@context": {
    "skos": "http://www.w3.org/2004/02/skos/core#",
    "notation": "skos:notation",
    "prefLabel": "skos:prefLabel"
  },
  "@type": "skos:Concept",
  "@id": "http://example.org/concept/2119",
  "notation": "2119",
  "prefLabel": "component"
}

The @context is auto-generated from the declared namespaces and predicates. No manual context definition is needed.

Language-Tagged Values

When a predicate is declared with lang_tagged: true, values are serialized with language information:

Turtle:

skos:prefLabel "component"@eng ;
skos:prefLabel "composant"@fra ;

JSON-LD:

"prefLabel": { "eng": "component", "fra": "composant" }

In JSON-LD, the @context auto-generates a language container:

"prefLabel": { "@id": "skos:prefLabel", "@container": "@language" }

For this to work, the attribute’s values must include the Lutaml::Rdf::LanguageTagged module (or be a Lutaml::Rdf::Literal). A simple value object is sufficient:

class LocalizedLiteral < Lutaml::Model::Serializable
  attribute :value, :string
  attribute :language_code, :string
end

URI Reference Predicates

Predicates declared with uri_reference: true serialize attribute values as URI references rather than string literals:

predicate :related, namespace: SkosNamespace, to: :related, uri_reference: true

Turtle:

Values are emitted as URI objects without quotes. Compact IRIs (e.g. "skos:other") are resolved to full URIs using the declared namespaces. Full URIs (e.g. "http://example.org/foo") are used as-is.

<http://example.org/concept/1> skos:related skos:other .

JSON-LD:

Values are wrapped in {"@id": …​} objects. The auto-generated @context includes a term definition with "@type": "@id":

{
  "@context": {
    "related": { "@id": "http://www.w3.org/2004/02/skos/core#related", "@type": "@id" }
  },
  "related": [ { "@id": "skos:other" } ]
}

Round-trip fidelity: compact IRI forms are preserved through serialization and deserialization. Values round-trip as Concept.from_turtle(concept.to_turtle) preserving the original "skos:other" compact form.

The uri_reference option is mutually exclusive with lang_tagged.

Graph Serialization

When a container model holds a collection of member resources, use members to serialize them as separate subjects in the same RDF graph.

Container Model

class Vocabulary < Lutaml::Model::Serializable
  attribute :id, :string
  attribute :concepts, Concept, collection: true

  rdf do
    namespace SkosNamespace

    subject { |v| "http://example.org/vocab/#{v.id}" }
    type "skos:ConceptScheme"
    predicate :prefLabel, namespace: SkosNamespace, to: :id

    members :concepts,
            predicate_name: :member,
            namespace: SkosNamespace
  end
end

The predicate_name and namespace parameters on members generate linking triples from the container to each member.

Turtle Output with Members

vocab = Vocabulary.new(id: "iso1087", concepts: [concept1, concept2])
puts vocab.to_turtle

Produces a single Turtle document with the container and all member triples:

@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

<http://example.org/vocab/iso1087> a skos:ConceptScheme;
  skos:prefLabel "iso1087";
  skos:member <http://example.org/concept/2119>;
  skos:member <http://example.org/concept/2120> .

<http://example.org/concept/2119> a skos:Concept;
  skos:notation "2119";
  skos:prefLabel "component" .

<http://example.org/concept/2120> a skos:Concept;
  skos:notation "2120";
  skos:prefLabel "intension" .

JSON-LD Output with Members

puts vocab.to_jsonld

Produces a JSON-LD document with @graph containing all resources:

{
  "@context": {
    "skos": "http://www.w3.org/2004/02/skos/core#",
    "prefLabel": { "@id": "skos:prefLabel" },
    "notation": { "@id": "skos:notation" },
    "member": { "@id": "http://www.w3.org/2004/02/skos/core#member", "@type": "@id" }
  },
  "@graph": [
    {
      "@id": "http://example.org/vocab/iso1087",
      "@type": "skos:ConceptScheme",
      "prefLabel": "iso1087",
      "member": [
        { "@id": "http://example.org/concept/2119" },
        { "@id": "http://example.org/concept/2120" }
      ]
    },
    {
      "@id": "http://example.org/concept/2119",
      "@type": "skos:Concept",
      "notation": "2119",
      "prefLabel": "component"
    },
    {
      "@id": "http://example.org/concept/2120",
      "@type": "skos:Concept",
      "notation": "2120",
      "prefLabel": "intension"
    }
  ]
}

Member-Only Models

A container model without a subject block serializes only the member triples. This is useful when you don’t need a container resource:

class MemberOnly < Lutaml::Model::Serializable
  attribute :items, Concept, collection: true

  rdf do
    namespace SkosNamespace
    members :items
  end
end

Namespace Merging

When a container and its members declare different namespaces, all namespaces are merged in the output. Prefix declarations in Turtle and @context entries in JSON-LD include the union of all namespaces.

Architecture

The unified RDF infrastructure consists of:

  • Lutaml::Rdf::Mapping — Unified mapping base class with namespace, subject, type, predicate, and members DSL methods

  • Lutaml::Rdf::MappingRule — Value object for predicate-to-attribute mappings

  • Lutaml::Rdf::MemberRule — Value object for members declarations

  • Lutaml::Rdf::Transform — Base transform class with shared logic for subject URI resolution, type resolution, and language extraction

  • Lutaml::Turtle::Transform — Inherits from Rdf::Transform, produces Turtle

  • Lutaml::JsonLd::Transform — Inherits from Rdf::Transform, produces JSON-LD

Both format-specific transforms detect Rdf::Mapping instances and dispatch to unified serialization logic.

Error Handling

  • Lutaml::Turtle::MissingSubjectError — raised when a model with predicates has no subject block and no members declaration

  • ArgumentError — raised when a predicate’s namespace is not a Rdf::Namespace subclass