Skip to content

ontolith.schema

ontolith.schema

Schema layer - meta-model, IR, and DSL support.

This module provides schema definition capabilities: - Internal Representation (IR) - canonical JSON format - Class DSL - Python class-based schema definition (M3) - LinkML-aligned YAML front-end (M3)

JSON module-attribute

JSON = Annotated[dict[str, Any], _ScalarMarker('JSON')]

A scalar JSON-valued property/attribute type in the class DSL — any value json.loads accepts (object, array, string, number, bool, or null), not only a JSON object despite the DSL annotation's dict[str, Any] typing (a caller-facing convenience, not what storage actually validates against).

URI module-attribute

URI = Annotated[str, _ScalarMarker('URI')]

A scalar URI-or-CURIE-valued property/attribute type in the class DSL (LinkML's uriorcurie, ADR-0013 — a full scheme:... URI or a prefix:local-name CURIE, either is accepted).

Boolean module-attribute

Boolean = Annotated[bool, _ScalarMarker('Boolean')]

A scalar boolean-valued property/attribute type in the class DSL.

Date module-attribute

Date = Annotated[str, _ScalarMarker('Date')]

A scalar ISO-8601 date-valued property/attribute type in the class DSL.

DateTime module-attribute

DateTime = Annotated[str, _ScalarMarker('DateTime')]

A scalar ISO-8601 datetime-valued property/attribute type in the class DSL.

Float module-attribute

Float = Annotated[float, _ScalarMarker('Float')]

A scalar float-valued property/attribute type in the class DSL.

Integer module-attribute

Integer = Annotated[int, _ScalarMarker('Integer')]

A scalar integer-valued property/attribute type in the class DSL.

Ref module-attribute

Ref = _RefAlias

Relation target type: Ref["ConceptName"], e.g. employer: Ref["Organization"] = Relation().

Text module-attribute

Text = Annotated[str, _ScalarMarker('Text')]

A scalar string-valued property/attribute type in the class DSL.

Concept

Base class for schema concepts (SPEC §6.2).

Example

class Person(Concept): name: Text born: Date | None = None

ConceptDef

Bases: BaseModel

Concept definition in the IR.

Attributes:

Name Type Description
name str

Concept name (e.g., "Person", "Organization")

properties dict[str, PropertyDef]

Property definitions

relations dict[str, RelationDef]

Relation definitions

description str | None

Optional description

validate_no_property_relation_name_collision

validate_no_property_relation_name_collision() -> (
    ConceptDef
)

Reject a field name declared in both properties and relations on the same concept (KI-040).

SchemaIR._resolve_field checks properties before relations, so an unrejected collision would silently make kind_of() (and value_type_of/temporality_of/cardinality_of) always resolve to the property, making the relation half permanently unreachable — every ref write to that name would be rejected as a kind mismatch it could never satisfy. Not reachable via the class DSL or the LinkML front-end today (both keep properties/relations in one namespace), only via a hand-built SchemaIR.

Source code in src/ontolith/schema/ir.py
@model_validator(mode="after")
def validate_no_property_relation_name_collision(self) -> "ConceptDef":
    """Reject a field name declared in both `properties` and
    `relations` on the same concept (KI-040).

    `SchemaIR._resolve_field` checks `properties` before `relations`,
    so an unrejected collision would silently make `kind_of()`
    (and `value_type_of`/`temporality_of`/`cardinality_of`) always
    resolve to the property, making the relation half permanently
    unreachable — every ref write to that name would be rejected as a
    kind mismatch it could never satisfy. Not reachable via the class
    DSL or the LinkML front-end today (both keep properties/relations
    in one namespace), only via a hand-built `SchemaIR`.
    """
    collisions = self.properties.keys() & self.relations.keys()
    if collisions:
        from ontolith.core.errors import SchemaError

        raise SchemaError(
            f"Concept {self.name!r} declares {sorted(collisions)} as both a "
            "property and a relation — a field name must be one or the other"
        )
    return self

PropertyDef

Bases: BaseModel

Property definition in the IR.

Attributes:

Name Type Description
name str

Property name

value_type Literal['Text', 'Integer', 'Float', 'Boolean', 'Date', 'DateTime', 'URI', 'JSON']

Type of value (Text, Integer, Date, etc.)

cardinality Literal['single', 'many']

Single value or many (SPEC §6)

required bool

Whether the property is required

temporality Literal['static', 'time_varying']

How conflicts are handled (static vs time_varying)

description str | None

Optional description

RelationDef

Bases: BaseModel

Relation definition in the IR.

Attributes:

Name Type Description
name str

Relation name

target_concept str

Target concept name

cardinality Literal['single', 'many']

Single reference or many (SPEC §6)

required bool

Whether the relation is required

temporality Literal['static', 'time_varying']

How conflicts are handled

inverse str | None

Inverse relation name (optional)

description str | None

Optional description

SchemaIR

Bases: BaseModel

Complete schema definition in Internal Representation.

This is the canonical format for schema. All DSLs compile to this.

Attributes:

Name Type Description
namespace str

Namespace this schema belongs to

version int

Schema version (monotonically increasing)

concepts dict[str, ConceptDef]

Concept definitions by name

metadata dict[str, Any]

Optional metadata

validate_relation_targets

validate_relation_targets() -> SchemaIR

Validate that all relation targets reference existing concepts.

Source code in src/ontolith/schema/ir.py
@model_validator(mode="after")
def validate_relation_targets(self) -> "SchemaIR":
    """Validate that all relation targets reference existing concepts."""
    for concept in self.concepts.values():
        for relation in concept.relations.values():
            if relation.target_concept not in self.concepts:
                from ontolith.core.errors import SchemaError

                raise SchemaError(
                    f"Relation {concept.name}.{relation.name} references "
                    f"unknown concept: {relation.target_concept}"
                )
    return self

has_predicate

has_predicate(predicate: str) -> bool

Return whether a dotted "Concept.field" predicate is declared in this schema.

Source code in src/ontolith/schema/ir.py
def has_predicate(self, predicate: str) -> bool:
    """Return whether a dotted "Concept.field" predicate is declared in this schema."""
    return self._resolve_field(predicate) is not None

has_concept

has_concept(concept: str) -> bool

Return whether concept (e.g. "Person") is declared in this schema (KI-090).

Source code in src/ontolith/schema/ir.py
def has_concept(self, concept: str) -> bool:
    """Return whether `concept` (e.g. "Person") is declared in this schema (KI-090)."""
    return concept in self.concepts

temporality_of

temporality_of(
    predicate: str,
) -> Literal["static", "time_varying"]

Resolve the declared temporality of a predicate (SPEC §10.1).

Parameters:

Name Type Description Default
predicate str

Dotted predicate, e.g. "Person.name" or "Person.employer"

required

Returns:

Type Description
Literal['static', 'time_varying']

The property's or relation's declared temporality, or "static"

Literal['static', 'time_varying']

(SPEC's stated default) if unresolvable.

Source code in src/ontolith/schema/ir.py
def temporality_of(self, predicate: str) -> Literal["static", "time_varying"]:
    """Resolve the declared temporality of a predicate (SPEC §10.1).

    Args:
        predicate: Dotted predicate, e.g. "Person.name" or "Person.employer"

    Returns:
        The property's or relation's declared temporality, or "static"
        (SPEC's stated default) if unresolvable.
    """
    field = self._resolve_field(predicate)
    return field.temporality if field is not None else "static"

cardinality_of

cardinality_of(predicate: str) -> Literal['single', 'many']

Resolve the declared cardinality of a predicate (SPEC §4, ADR-0017).

Parameters:

Name Type Description Default
predicate str

Dotted predicate, e.g. "Person.name" or "Person.phone"

required

Returns:

Type Description
Literal['single', 'many']

The property's or relation's declared cardinality, or "single"

Literal['single', 'many']

(the schema default) if unresolvable.

Source code in src/ontolith/schema/ir.py
def cardinality_of(self, predicate: str) -> Literal["single", "many"]:
    """Resolve the declared cardinality of a predicate (SPEC §4, ADR-0017).

    Args:
        predicate: Dotted predicate, e.g. "Person.name" or "Person.phone"

    Returns:
        The property's or relation's declared cardinality, or "single"
        (the schema default) if unresolvable.
    """
    field = self._resolve_field(predicate)
    return field.cardinality if field is not None else "single"

value_type_of

value_type_of(predicate: str) -> str | None

Resolve the declared value_type of a literal property predicate (SPEC §4, KI-031).

Parameters:

Name Type Description Default
predicate str

Dotted predicate, e.g. "Person.name" or "Person.age"

required

Returns:

Type Description
str | None

The declared value_type, or None if the predicate is

str | None

unresolvable or resolves to a RelationDef — relations have no

str | None

value_type. Use kind_of() to check a predicate's kind

str | None

directly (KI-040) rather than inferring it from a None here.

Source code in src/ontolith/schema/ir.py
def value_type_of(self, predicate: str) -> str | None:
    """Resolve the declared value_type of a literal property predicate
    (SPEC §4, KI-031).

    Args:
        predicate: Dotted predicate, e.g. "Person.name" or "Person.age"

    Returns:
        The declared value_type, or None if the predicate is
        unresolvable or resolves to a RelationDef — relations have no
        value_type. Use `kind_of()` to check a predicate's kind
        directly (KI-040) rather than inferring it from a `None` here.
    """
    field = self._resolve_field(predicate)
    return field.value_type if isinstance(field, PropertyDef) else None

kind_of

kind_of(
    predicate: str,
) -> Literal["property", "relation"] | None

Resolve whether a predicate is declared a property or a relation (SPEC §4, KI-040).

Parameters:

Name Type Description Default
predicate str

Dotted predicate, e.g. "Person.name" or "Person.employer"

required

Returns:

Type Description
Literal['property', 'relation'] | None

"property" or "relation", or None if the predicate is

Literal['property', 'relation'] | None

unresolvable (schema-less namespace, or not declared in this

Literal['property', 'relation'] | None

schema version).

Source code in src/ontolith/schema/ir.py
def kind_of(self, predicate: str) -> Literal["property", "relation"] | None:
    """Resolve whether a predicate is declared a property or a relation
    (SPEC §4, KI-040).

    Args:
        predicate: Dotted predicate, e.g. "Person.name" or "Person.employer"

    Returns:
        `"property"` or `"relation"`, or `None` if the predicate is
        unresolvable (schema-less namespace, or not declared in this
        schema version).
    """
    field = self._resolve_field(predicate)
    if field is None:
        return None
    return "property" if isinstance(field, PropertyDef) else "relation"

to_json

to_json() -> dict[str, Any]

Serialize to JSON-compatible dict.

Source code in src/ontolith/schema/ir.py
def to_json(self) -> dict[str, Any]:
    """Serialize to JSON-compatible dict."""
    return self.model_dump()

from_json classmethod

from_json(data: dict[str, Any]) -> SchemaIR

Deserialize from JSON-compatible dict.

Source code in src/ontolith/schema/ir.py
@classmethod
def from_json(cls, data: dict[str, Any]) -> "SchemaIR":
    """Deserialize from JSON-compatible dict."""
    return cls.model_validate(data)

Property

Property(
    *,
    cardinality: Cardinality = "single",
    required: bool | None = None,
    temporality: Temporality = "static",
    description: str | None = None,
) -> Any

Field spec for a property, used as a class-body default.

Plain annotations (name: Text) are sufficient for static, single-valued, optional properties. Use Property(...) to declare temporality="time_varying", non-default cardinality, or a description on a property.

required defaults to None, meaning "infer from the annotation" — a bare Integer is required, Integer | None is optional — exactly as if no Property(...) spec were attached at all. Pass required=True/False explicitly only to override what the annotation itself already states.

Example

salary: Integer = Property(temporality="time_varying")

Source code in src/ontolith/schema/dsl.py
def Property(
    *,
    cardinality: Cardinality = "single",
    required: bool | None = None,
    temporality: Temporality = "static",
    description: str | None = None,
) -> Any:
    """Field spec for a property, used as a class-body default.

    Plain annotations (`name: Text`) are sufficient for static, single-valued,
    optional properties. Use `Property(...)` to declare `temporality="time_varying"`,
    non-default cardinality, or a description on a property.

    `required` defaults to `None`, meaning "infer from the annotation" — a bare
    `Integer` is required, `Integer | None` is optional — exactly as if no
    `Property(...)` spec were attached at all. Pass `required=True`/`False`
    explicitly only to override what the annotation itself already states.

    Example:
        salary: Integer = Property(temporality="time_varying")
    """
    return _PropertySpec(
        cardinality=cardinality,
        required=required,
        temporality=temporality,
        description=description,
    )

Relation

Relation(
    *,
    inverse: str | None = None,
    cardinality: Cardinality = "single",
    required: bool | None = None,
    temporality: Temporality = "static",
    description: str | None = None,
) -> Any

Field spec for a relation, used as a class-body default (SPEC §6.2).

required defaults to None, meaning "infer from the annotation" — a bare Ref["X"] is required, Ref["X"] | None is optional — exactly like a relation with no Relation(...) spec at all. Pass required=True/False explicitly only to override what the annotation itself already states.

Example

employer: Ref["Organization"] = Relation(inverse="employees")

Source code in src/ontolith/schema/dsl.py
def Relation(
    *,
    inverse: str | None = None,
    cardinality: Cardinality = "single",
    required: bool | None = None,
    temporality: Temporality = "static",
    description: str | None = None,
) -> Any:
    """Field spec for a relation, used as a class-body default (SPEC §6.2).

    `required` defaults to `None`, meaning "infer from the annotation" — a
    bare `Ref["X"]` is required, `Ref["X"] | None` is optional — exactly like
    a relation with no `Relation(...)` spec at all. Pass `required=True`/`False`
    explicitly only to override what the annotation itself already states.

    Example:
        employer: Ref["Organization"] = Relation(inverse="employees")
    """
    return _RelationSpec(
        inverse=inverse,
        cardinality=cardinality,
        required=required,
        temporality=temporality,
        description=description,
    )

compile_schema

compile_schema(
    namespace: str,
    version: int,
    *concepts: type[Concept],
    metadata: dict[str, Any] | None = None,
) -> SchemaIR

Compile Concept subclasses into a SchemaIR (SPEC §6.1: classes -> IR).

Explicit — takes the concepts to include rather than tracking every Concept subclass ever defined in a hidden global registry.

Parameters:

Name Type Description Default
namespace str

Namespace this schema belongs to

required
version int

Schema version (monotonically increasing)

required
concepts type[Concept]

Concept subclasses to compile

()
metadata dict[str, Any] | None

Optional schema-level metadata

None

Returns:

Type Description
SchemaIR

SchemaIR containing the compiled concepts

Raises:

Type Description
SchemaError

If a relation references a concept not among concepts

Source code in src/ontolith/schema/dsl.py
def compile_schema(
    namespace: str,
    version: int,
    *concepts: type[Concept],
    metadata: dict[str, Any] | None = None,
) -> SchemaIR:
    """Compile Concept subclasses into a SchemaIR (SPEC §6.1: classes -> IR).

    Explicit — takes the concepts to include rather than tracking every
    `Concept` subclass ever defined in a hidden global registry.

    Args:
        namespace: Namespace this schema belongs to
        version: Schema version (monotonically increasing)
        concepts: Concept subclasses to compile
        metadata: Optional schema-level metadata

    Returns:
        SchemaIR containing the compiled concepts

    Raises:
        SchemaError: If a relation references a concept not among `concepts`
    """
    concept_defs: dict[str, ConceptDef] = {}
    for concept_cls in concepts:
        concept_def = getattr(concept_cls, "__ontolith_concept_def__", None)
        if concept_def is None:
            raise SchemaError(f"{concept_cls!r} is not a compiled Concept subclass")
        concept_defs[concept_def.name] = concept_def

    return SchemaIR(
        namespace=namespace,
        version=version,
        concepts=concept_defs,
        metadata=metadata or {},
    )

generate_class_stubs

generate_class_stubs(schema: SchemaIR) -> str

Decompile a SchemaIR into Concept subclass source text (SPEC §6.1: IR -> classes).

Output is deterministic (concepts sorted by name, fields in declaration order as stored in the IR dicts) so repeated calls on the same IR are byte-identical.

Parameters:

Name Type Description Default
schema SchemaIR

SchemaIR to decompile

required

Returns:

Type Description
str

Formatted Python source defining one Concept subclass per concept

Source code in src/ontolith/schema/dsl.py
def generate_class_stubs(schema: SchemaIR) -> str:
    """Decompile a SchemaIR into Concept subclass source text (SPEC §6.1: IR -> classes).

    Output is deterministic (concepts sorted by name, fields in declaration order
    as stored in the IR dicts) so repeated calls on the same IR are byte-identical.

    Args:
        schema: SchemaIR to decompile

    Returns:
        Formatted Python source defining one Concept subclass per concept
    """
    lines = [
        '"""Generated by ontolith.schema.dsl.generate_class_stubs. Do not edit by hand."""',
        "",
        "from ontolith import Concept, Property, Ref, Relation",
        "from ontolith import Boolean, Date, DateTime, Float, Integer, JSON, Text, URI",
        "",
    ]
    for concept_name in sorted(schema.concepts):
        concept = schema.concepts[concept_name]
        lines.append("")
        lines.append(f"class {concept_name}(Concept):")
        body_lines: list[str] = []
        if concept.description:
            # repr(), not a raw triple-quoted string: a description containing
            # quotes or backslashes would otherwise produce invalid Python.
            body_lines.append(f"    {concept.description!r}")
        for prop_name in concept.properties:
            prop = concept.properties[prop_name]
            body_lines.append(f"    {prop_name}: {_property_annotation(prop)}")
        for rel_name in concept.relations:
            rel = concept.relations[rel_name]
            body_lines.append(f"    {rel_name}: {_relation_field(rel)}")
        if not body_lines:
            body_lines.append("    pass")
        lines.extend(body_lines)
    lines.append("")
    return "\n".join(lines)