Linked data class python package for object oriented linked data (OO-LD) based on pydantic. This package aims to implement this functionality independent from the osw-python package - work in progress.
With uv (recommended):
uv add oold
Or with pip:
pip install oold
- lossless transpilation between OO-LD schemas and extended pydantic data classes
- interpret string IRIs with
oold-rangeannotation as typed class property - dynamically resolve such IRIs from one or multiple backends (simple in-memory dict, RDF-Graph, SPARQL-Endpoint, Document Store, etc.)
- serialize class instances to JSON-LD while replacing python object-references with IRIs
- apply filters / queries to backend-requests (SPARQL, GraphQL, ...)
| Library | Notes |
|---|---|
| RDFLib | RDF management; no schema validation or type safety. Used as a backend by oold-python. |
| SuRF | ORM-like RDF; dynamically generated classes, no static type checking. |
| Owlready2 | OWL-aligned classes with native reasoning; no remote SPARQL support. |
| twa | Pydantic-based OGM; tightly couples RDF properties and type annotations. |
| COLD | Generates static classes from OWL; no object-to-graph mapping. |
See also: Bai et al. https://doi.org/10.1039/D5DD00069F
Generate Python data models from OO-LD Schemas (based on datamodel-code-generator):
from oold.generator import Generator import importlib import datamodel_code_generator import oold.model.model as model schemas = [ { # minimal example "id": "Foo", "title": "Foo", "type": "object", "properties": { "id": {"type": "string"}, }, }, ] g = Generator() g.generate(schemas, main_schema="Foo.json", output_model_type=datamodel_code_generator.DataModelType.PydanticBaseModel) importlib.reload(model) # Now you can work with your generated model f = model.Foo(id="ex:f") print(f)
This example uses the built-in Generator to create a basic Pydantic model (v1 or v2) from JSON schemas.
More details see example code
Illustrative example how the object orient linked data (OO-LD) package provides an abstract knowledge graph (KG) interface. First (line 3) primary schemas (Foo) and their dependencies (Bar, Baz) are loaded from the KG and transformed into python dataclasses. Instantiation of foo is handled by loading the respective JSON(-LD) document from the KG and utilizing the type relation to the corresponding schema and dataclass (line 5). Because bar is not a dependent subobject of foo it is loaded on-demand on first access of the corresponding class attribute of foo (foo.bar in line 7), while id as dependent literal is loaded immediately in the same operation. In line 9 baz is constructed by an existing controller class subclassing Foo and finally stored as a new entity in the KG in line 11.
Represent your domain objects easily and reference them via IRIs or direct object instances. For instance, if you have a Foo model referencing a Bar model:
import oold.model.model as model # Create a Foo object linked to Bar f = model.Foo( id="ex:f", literal="test1", b=model.Bar(id="ex:b", prop1="test2"), b2=[model.Bar(id="ex:b1", prop1="test3"), model.Bar(id="ex:b2", prop1="test4")], ) print(f.b.id) # ex:b print(f.b2[0].prop1) # test3
You can also refer to objects by IRI:
# Assign IRI strings directly f = model.Foo( id="ex:f", literal="test1", b="ex:b", # automatically resolved to a Bar object b2=["ex:b1", "ex:b2"], )
Thanks to the resolver mechanism, these IRIs turn into fully-fledged objects as soon as you need them.
Writing a model by hand, declare links with Link[T] / LinkList[T]. This is the recommended notation: it is the only one a type checker reads correctly in both directions - the resolved object you get back, and the object, IRI or JSON object you may assign:
from oold.model import Link, LinkedBaseModel, LinkList, OoldField class Person(LinkedBaseModel): id: str name: str | None = None employer: Link["Organization | None"] = OoldField() knows: LinkList["Person"] = OoldField() alice = Person(id="ex:alice", knows=["ex:bob", {"id": "ex:carol"}]) alice.knows[0].name # a Person, resolved on access
Existing declarations keep working unchanged, including the range form code generation emits.
More details see example code and Object Graph Mapping.
Easily convert your objects to RDF (JSON-LD) and integrate with SPARQL queries:
from rdflib import Graph from typing import List, Optional # Example: Convert Person objects to RDF p1 = model.Person(name="Alice") p2 = model.Person(name="Bob", knows=[p1]) # Export to JSON-LD print(p2.to_jsonld()) # Load into RDFlib g = Graph() g.parse(data=p1.to_jsonld(), format="json-ld") g.parse(data=p2.to_jsonld(), format="json-ld") # Perform SPARQL queries qres = g.query(""" SELECT ?name WHERE { ?s <https://schema.org/knows> ?o . ?o <https://schema.org/name> ?name . } """) for row in qres: print("Bob knows", row.name)
The extended dataclass notation includes semantic annotations as JSON-LD context, giving you powerful tooling for knowledge graphs, semantic queries, and data interoperability.
More details see example code
Base mixin for controllers that extend LinkedBaseModel data classes. Controllers add runtime behavior (connections, archiving, state) without polluting the data model.
from oold.model import BaseController, LinkedBaseModel class Robot(LinkedBaseModel): name: str joint_count: int = 6 connection_url: str = "" class RobotController(BaseController, Robot): _connected: bool = False def connect(self): self._connected = True print(f"Connected to {self.connection_url}") def move(self, joint: int, angle: float): if not self._connected: raise RuntimeError("Not connected") print(f"Moving joint {joint} to {angle} deg") ctrl = RobotController(name="arm-1", connection_url="tcp://192.168.1.10:5000") ctrl.connect() ctrl.move(1, 45.0) # Serialization includes model fields, strips controller state ctrl.to_json() # {"name": "arm-1", "joint_count": 6, "connection_url": "tcp://..."}
Key features:
- Auto-detects the pure data model from MRO - no manual configuration needed
- Serialization strips controller fields -
to_json()/to_jsonld()only include data model fields - Type registry exclusion - controllers don't replace their data model in the
_typeslookup, so backend resolution always returns the pure model class - Multi-model support -
Controller(ModelA, ModelB)merges type arrays from both models
Convert between model classes, preserving __iris__ references:
target = source.cast(TargetClass, remove_extra=True, none_to_default=True) # Or construct directly from another model instance: target = TargetClass(source, extra_field="value")
Parameters:
none_to_default- drop None/empty list attributes so the target uses its defaultsremove_extra- drop fields not defined on the target classsilent- suppress warnings about dropped fields (default: True)
Built-in backends for entity persistence and resolution:
| Backend | Storage | Query support |
|---|---|---|
| SimpleDictDocumentStore | In-memory dict, optional JSON file (file_path) |
Filter by field |
| SqliteDocumentStore | SQLite database (default format=JSON) |
- |
| LocalSparqlBackend | In-memory RDF graph (rdflib) | SPARQL |
from oold.backend.document_store import SimpleDictDocumentStore from oold.backend.interface import StoreParam, SetResolverParam, set_resolver store = SimpleDictDocumentStore(file_path="./entities.json") set_resolver(SetResolverParam(iri="ex", resolver=store)) # Store and resolve entities store.store(StoreParam(nodes={"ex:foo": foo})) loaded = MyModel["ex:foo"] # resolves via registered backend
Custom backends implement the Backend interface (resolve_iris, store_json_dicts).
Check that an OO-LD schema is well formed, and that its @context actually carries every
declared property into RDF. A property declared in properties but missing from @context is
neither a JSON Schema error nor a JSON-LD error - it just quietly disappears, and the data loses
meaning.
pip install "oold[validation]" oold validate Person.schema.json # one schema oold validate ./schemas/ # a whole directory oold validate-instance doc.instance.json # a document against the schema it names
Exit code is 0 only when every check passes, so it drops straight into CI. The same pipeline is
available as a Python API and as an MCP server (oold[mcp]).
It is a native port of the reference harness in oold-schema, verified to agree with it on verdicts, and it validates against versioned meta-schemas - a released version, the unreleased upstream state, or several at once. See docs/how-to/validation.md.
This project uses uv and make. Clone and set up:
git clone https://github.com/OO-LD/oold-python cd oold-python make install # create the venv and install pre-commit hooks
Common tasks:
make check # ruff (lint+format), ty (types), deptry (deps) make test # run the test suite with coverage make docs # build and serve the documentation locally
See docs/contributing.md for the full workflow.
uv run tox -e benchmark # run benchmarks uv run tox -e benchmark-compare # compare against the previous run without storing results
We welcome contributions! Fork the repository, enable the pre-commit hooks, and open a pull request:
pre-commit install
Please also enable GitHub Actions on your fork so the test suite runs automatically.

