oxixml-model’s own documentation has claimed since 0.1.0 that code written against oxrdf ports over by changing only the use path. Until today, that claim was not true.
Today we released OxiXML 0.1.2 — a deliberately breaking release that closes every documented gap between oxixml-model’s public API and its oxrdf / oxttl / oxrdfxml / oxjsonld counterparts, checked signature-by-signature against oxrdf 0.3.3, oxttl 0.2.3, oxrdfxml 0.2.3, oxjsonld 0.2.5, oxrdfio 0.2.5, and spargebra 0.4.6.
No C, no C++, no Fortran — still the same zero-external-crates-by-default Pure-Rust workspace introduced at 0.1.0 and hardened at 0.1.1. 0.1.2 breaks compatibility with 0.1.1 on purpose and ships no compatibility shims: 0.1.1 is superseded, and 0.1.2 is the version to depend on. It compiles to a single static binary, targets WASM, and runs everywhere.
Why OxiXML 0.1.2 is a game changer
At 0.1.1, oxixml-model looked like oxrdf on the surface and diverged the moment real code ran against it:
- Iteration handed back owned terms where
oxrdfhands back borrows.for triple in &graphcompiled —&Graphwas alreadyIntoIterator— but every item was an ownedTriple, so a loop ported fromoxrdfallocated on every single iteration instead of the zero-copy borrow the same code got upstream. - Builder options took
&strwhere every upstream builder takesimpl Into<String>. AStringbuilt for the occasion couldn’t be moved intowith_prefix/with_base_iri— generic code holding one had to borrow it back down first. - The JSON-LD parsers could not cross a thread boundary.
ReaderJsonLdParser,SliceJsonLdParser, andTokioAsyncReaderJsonLdParserboxed their document loader asBox<dyn DocumentLoader>with noSendbound — the ordinary move onto a worker thread to read a document off the request path simply didn’t compile. JsonLdSerializer::with_prefix/with_base_iriwere a no-op. Both were validated and stored, and then silently ignored: no@contextwas ever written, no matter what a caller configured.
OxiXML 0.1.2 ends all of that:
- Every documented
oxrdf-parity divergence, closed.Graph::iter,Dataset::iter, everyGraphView/GraphViewMutiterator, and all the pattern queries (triples_for_subject/_predicate/_object,triples_for_pattern,objects_for_subject_predicate,predicates_for_subject_object,subjects_for_predicate_object) now yieldTripleRef<'_>/QuadRef<'_>/TermRef<'_>/NamedNodeRef<'_>/NamedOrBlankNodeRef<'_>instead of owned terms. The interning layer decodes by borrowing from its own string table, so this iteration allocates nothing at all. - Mutators and vocabulary constants follow the same shape.
Graph::insert,Dataset::insert, andGraphViewMut::inserttakeimpl Into<TripleRef<'a>>/impl Into<QuadRef<'a>>;vocab::{rdf, rdfs, xsd, geosparql}constants areCopyNamedNodeRef<'static>instead of ownedNamedNode. with_prefix/with_base_iritakeimpl Into<String>acrossTurtleParser/Serializer,TriGParser/Serializer,N3Parser,RdfXmlParser/Serializer,JsonLdParser, andoxixml-io’sRdfParser/RdfSerializer.- The JSON-LD parsers are
Send. The document loader is nowBox<dyn DocumentLoader + Send>, matching the boundoxjsonldalready puts on its ownLoadDocumentCallback. JsonLdSerializer::with_prefix/with_base_iriactually emit the envelope now —{"@context": {…}, "@graph": […]},@basefirst, prefixes in name order (the empty prefix written as@vocab), node identifiers made relative to the base.
Technical Deep Dive: closing the gap signature by signature
- Interning is what makes the borrow free.
oxixml-model’s string table already existed at 0.1.1; 0.1.2 changes what leaves it.Graph/Datasetiteration, every pattern query, and the vocabulary constants now decode straight out of that table as*Ref<'_>types — a borrow into the interned store, not a freshTriple/Quad/NamedNodebuilt per item. - One trait,
IntoOxString, unifies every&strconstructor.NamedNode::new,BlankNode::new,Variable::new,Literal::new_simple_literal/new_typed_literal, and the language-tagged constructors used to requireInto<OxString>— andOxStringisOxStr<'static>, so any&strthat didn’t happen to live for'staticwas rejected outright, which is the single most common thing a port fromoxrdf’simpl Into<String>would hit.IntoOxStringcopies a borrowed&strand moves an ownedOxString/Stringwithout a new allocation, so the zero-copy paths this crate already had keep working exactly as before. - The serializer tier follows the model tier.
rdf/oxixml-turtle,-rdfxml,-jsonld,-trix, and-io’sserialize_triple/serialize_quad— blocking, async, and low-level — now takeimpl Into<TripleRef<'a>>/impl Into<QuadRef<'a>>instead of&Triple/&Quad, so a graph parsed, filtered, and re-serialized through the same borrowed loop never clones a term between read and write. Existing&ownedcall sites keep compiling unchanged. - What’s still deliberately different from
oxrdf— and now says so. Three gaps remain, documented inoxixml-model’s own crate docs instead of left for a reader to discover:Termadditionally implementsPartialEqagainstNamedNode/BlankNode/Literal, which makesterm == x.into()ambiguous and needsterm == Term::from(x); owned accessors returnOxStringrather thanString; andoxrdf’s deprecatedis_plain/LiteralRef::destructaren’t provided.
Getting Started
cargo add oxixml-model oxixml-turtle
use oxixml_model::{Graph, vocab::rdf};
use oxixml_turtle::TurtleParser;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = r#"@prefix schema: <http://schema.org/> .
<http://example.com/alice> a schema:Person ;
schema:name "Alice" ."#;
// Parse into an owned Graph.
let mut graph = Graph::new();
for triple in TurtleParser::new().for_reader(file.as_bytes()) {
graph.insert(&triple?);
}
// New in 0.1.2: iterating a borrowed &Graph yields TripleRef<'_>,
// not an owned Triple — no clone per item, matching oxrdf.
for triple in &graph {
if triple.predicate == rdf::TYPE {
println!("{} is a {}", triple.subject, triple.object);
}
}
// Need to keep one past the graph's lifetime? Ask for it explicitly.
let first_owned = graph.iter().next().map(|t| t.into_owned());
assert!(first_owned.is_some());
Ok(())
}
Or through the facade, with only the features you need:
[dependencies]
oxixml = { version = "0.1.2", features = ["turtle"] }
What’s New in 0.1.2
Breaking
oxixml-modeloxrdf/oxttlAPI parity: borrowed-ref iteration and pattern queries, borrowed mutators (insert,graph_mut),Copyborrowed vocabulary constants, the newIntoOxString-based&strconstructors, andcanonicalize_blank_nodesreturningHashMap<BlankNodeRef<'_>, BlankNode>.oxixml-turtle/-rdfxml/-jsonld/-trix/-ioserializers takeimpl Into<TripleRef<'a>>/impl Into<QuadRef<'a>>instead of&Triple/&Quad.with_prefix/with_base_iritakeimpl Into<String>instead of&stracross the Turtle, TriG, N3, RDF/XML, and JSON-LD parsers/serializers andoxixml-io’s umbrella types.oxixml-io’sRdfParser::with_document_loaderclosure now also requiresSend + Sync + UnwindSafe + RefUnwindSafe(previously'staticalone).oxixml-jsonld’sReaderJsonLdParser/SliceJsonLdParser/TokioAsyncReaderJsonLdParserareSend.
Fixed
oxixml-jsonld:JsonLdSerializer::with_prefix/with_base_irinow actually produce the{"@context": …, "@graph": …}envelope instead of being silently ignored.oxixml-turtle:N3Term’sFrom<NamedNodeRef<'_>>import was gated to therdf-12feature while the impl itself was unconditional, which broke the crate’s default-feature build; the import is now unconditional too.
Full itemized detail is in the CHANGELOG.
Tips
- Porting a loop from
oxrdf? Drop the.clone().for triple in &graphnow yieldsTripleRef<'_>directly — if you were cloning to satisfy the old owned-Tripleitem type, that clone is dead code now. - Need to keep a term past the graph’s lifetime? Call
.into_owned(). Every*Reftype —TripleRef,QuadRef,TermRef,NamedNodeRef,BlankNodeRef— has one; that’s the one line a genuinely owned copy costs now. - A vocabulary constant now converts by value.
rdf::TYPE/rdfs::LABEL/etc. areCopy, so pass them directly anywhere animpl Into<NamedNodeRef<'_>>is expected; reach forrdf::TYPE.into_owned()only where the call genuinely needs an ownedNamedNode. - Building a prefix or base IRI on the fly? It moves in now.
with_prefix(format!("ex{i}"))andwith_base_iri(computed_iri)compile directly — no more borrowing a freshly builtStringback down to&str. - Reading documents off the request path? The JSON-LD parsers can finally move.
ReaderJsonLdParser/SliceJsonLdParser/TokioAsyncReaderJsonLdParserareSendas long as yourDocumentLoader/with_load_document_callbackclosure is — the ordinary case, since only a genuinely thread-hostile loader isn’t. - Set
with_prefix/with_base_irionJsonLdSerializerand re-check your output. If you configured either and silently got a bare array before, you now get the real{"@context": …, "@graph": …}envelope — a previously-inert option is live for the first time.
This is the foundation
OxiXML depends on nothing else in the ecosystem, so any project can adopt it without a dependency cycle. The ecosystem-wide migration wave that landed the same day as this release already put OxiRS, OxiEphemeris, TensorLogic, and OxiFY — alongside OxiGeo, OxiMedia, Legalis-RS, and the rest of the seventeen COOLJAPAN projects that used to pull quick-xml or an Oxigraph crate directly — on top of OxiXML tier by tier. 0.1.2 is what turns the next hop, from 0.1.1’s owned-iterator shape to this release’s borrowed one, into a routine version bump instead of a rewrite.
Repository: https://github.com/cool-japan/oxixml
Star the repo if you want an RDF stack that is drop-in compatible with oxrdf in fact, not just in the crate docs’ claim — zero-allocation iteration included.
The era of a migration guide that breaks on the first borrowed loop is over. Pure Rust XML and RDF — complete, conformant, and sovereign — is here.
— KitaSan at COOLJAPAN OÜ August 10, 2026