Skip to content

Ontology API

The semantic layer (pycharter.semantic.ontology) provides types, parsers, and validators for field-level semantic annotations, concept vocabularies, and lineage. Field mappings (ontology) can be stored with data contracts and used for validation and governance.

See the Ontology guide for the conceptual overview.

Core models

FieldMapping dataclass

FieldMapping(version: str, domain_schema: str | None = None, field_bindings: dict[str, FieldBinding | FieldConceptBinding] = dict())

A versioned field mapping artifact for a data contract.

Maps data contract fields to ontology concepts. Supports two formats:

  • New format: domain_schema + field_bindings as FieldBinding
  • Legacy format: field_bindings as FieldConceptBinding

Attributes:

Name Type Description
version str

Field mapping version string.

domain_schema str | None

Reference to a published LinkML schema in "name:version" format. None for legacy artifacts.

field_bindings dict[str, FieldBinding | FieldConceptBinding]

Mapping of field path to its semantic annotation. Values may be FieldBinding (new) or FieldConceptBinding (legacy).

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/semantic/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    result: dict[str, Any] = {"version": self.version}
    if self.domain_schema:
        result["domain_schema"] = self.domain_schema
    result["field_bindings"] = {
        name: fb.to_dict() for name, fb in self.field_bindings.items()
    }
    return result

FieldBinding dataclass

FieldBinding(class_name: str, slot_name: str, tags: tuple[str, ...] = (), owner: str | None = None, lineage: Lineage | None = None)

Binds a physical schema field to a LinkML class and slot.

This is the class/slot-centric replacement for FieldConceptBinding. Used by the new ontology artifact format that references a published LinkML domain schema.

Attributes:

Name Type Description
class_name str

LinkML class name the field belongs to.

slot_name str

LinkML slot name within that class.

tags tuple[str, ...]

Additional field-level tags.

owner str | None

Team or person owning this field.

lineage Lineage | None

Data lineage information.

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/semantic/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    result: dict[str, Any] = {
        "class": self.class_name,
        "slot": self.slot_name,
    }
    if self.tags:
        result["tags"] = list(self.tags)
    if self.owner is not None:
        result["owner"] = self.owner
    if self.lineage is not None:
        result["lineage"] = self.lineage.to_dict()
    return result

FieldConceptBinding dataclass

FieldConceptBinding(concept: str, broader: str | None = None, definition: str | None = None, tags: tuple[str, ...] = (), owner: str | None = None, description: str | None = None, deprecated: bool = False, lineage: Lineage | None = None, relationships: tuple[Relationship, ...] = ())

Semantic annotation for a single field.

Attributes:

Name Type Description
concept str

The semantic concept this field represents.

broader str | None

Parent concept in the hierarchy (deprecated -- use ontologies.yaml).

definition str | None

Human-readable definition (deprecated -- use ontologies.yaml).

tags tuple[str, ...]

Tags for categorization.

owner str | None

Team or person owning this field.

description str | None

Field-specific business description.

deprecated bool

Whether this field mapping is deprecated.

lineage Lineage | None

Data lineage information.

relationships tuple[Relationship, ...]

Relationships to other fields.

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/semantic/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    result: dict[str, Any] = {
        "concept": self.concept,
    }
    if self.broader is not None:
        result["broader"] = self.broader
    if self.definition is not None:
        result["definition"] = self.definition
    if self.tags:
        result["tags"] = list(self.tags)
    if self.owner is not None:
        result["owner"] = self.owner
    if self.description is not None:
        result["description"] = self.description
    if self.deprecated:
        result["deprecated"] = self.deprecated
    if self.lineage is not None:
        result["lineage"] = self.lineage.to_dict()
    if self.relationships:
        result["relationships"] = [r.to_dict() for r in self.relationships]
    return result

Concept dataclass

Concept(id: str, label: str, definition: str | None = None, broader: str | None = None, tags: tuple[str, ...] = (), alt_labels: tuple[str, ...] = (), notation: str | None = None, examples: tuple[str, ...] = (), exact_match: str | None = None, deprecated: bool = False, replaced_by: str | None = None, concept_type: str | None = None)

Full definition of a single concept in the vocabulary.

Mirrors the extended ontologies.yaml schema and can be losslessly converted to/from SKOS JSON-LD.

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary (round-trip safe with YAML schema).

Source code in src/pycharter/semantic/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary (round-trip safe with YAML schema)."""
    result: dict[str, Any] = {"id": self.id, "label": self.label}
    if self.definition is not None:
        result["definition"] = self.definition
    if self.broader is not None:
        result["broader"] = self.broader
    if self.tags:
        result["tags"] = list(self.tags)
    if self.alt_labels:
        result["alt_labels"] = list(self.alt_labels)
    if self.notation is not None:
        result["notation"] = self.notation
    if self.examples:
        result["examples"] = list(self.examples)
    if self.exact_match is not None:
        result["exact_match"] = self.exact_match
    if self.deprecated:
        result["deprecated"] = self.deprecated
    if self.replaced_by is not None:
        result["replaced_by"] = self.replaced_by
    if self.concept_type is not None:
        result["concept_type"] = self.concept_type
    return result

ConceptScheme dataclass

ConceptScheme(id: str, title: str, version: str, description: str = '', base_uri: str | None = None, concepts: list[Concept] = list())

A complete concept scheme (vocabulary).

Mirrors the top-level scheme: + concepts: structure in ontologies.yaml. Mutable because concepts are appended during parsing.

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/semantic/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    result: dict[str, Any] = {
        "scheme": {
            "id": self.id,
            "title": self.title,
            "version": self.version,
        },
        "concepts": [c.to_dict() for c in self.concepts],
    }
    if self.description:
        result["scheme"]["description"] = self.description
    if self.base_uri:
        result["scheme"]["base_uri"] = self.base_uri
    return result

Lineage dataclass

Lineage(derived_from: str | None = None, source_system: str | None = None)

Tracks the origin of a field.

Attributes:

Name Type Description
derived_from str | None

Field this was derived from (if any).

source_system str | None

System that produces this field.

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/semantic/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    return {
        "derived_from": self.derived_from,
        "source_system": self.source_system,
    }

Relationship dataclass

Relationship(target: str, type: str)

A relationship between two fields.

Attributes:

Name Type Description
target str

Target field path (e.g., "customers.id").

type str

Type of relationship (string, validated against schema).

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/semantic/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    return {
        "target": self.target,
        "type": self.type,
    }

Enumerations

ConceptTier

Bases: StrEnum

Built-in governance tiers for concepts.

These are the default tiers shipped with pycharter. Custom tiers can be added via the ontology schema without modifying this enum. Use schema_loader.validate_tier() for runtime validation.

CORE class-attribute instance-attribute

CORE = 'core'

Tier 1: Core concepts governed by central team.

DOMAIN class-attribute instance-attribute

DOMAIN = 'domain'

Tier 2: Domain concepts governed by domain teams.

FREE class-attribute instance-attribute

FREE = 'free'

Tier 3: Free-form concepts with no governance.

ConceptType

Bases: StrEnum

Built-in concept types for the knowledge graph.

These are the default types shipped with pycharter. Custom types can be added via the ontology schema without modifying this enum. Use schema_loader.validate_concept_type() for runtime validation.

ENTITY class-attribute instance-attribute

ENTITY = 'entity'

A thing with identity (customer, product, system).

PROCESS class-attribute instance-attribute

PROCESS = 'process'

A sequence of steps (checkout, onboarding).

EVENT class-attribute instance-attribute

EVENT = 'event'

Something that happens at a point in time.

ATTRIBUTE class-attribute instance-attribute

ATTRIBUTE = 'attribute'

A property or measurement.

RULE class-attribute instance-attribute

RULE = 'rule'

A constraint or policy.

SYSTEM class-attribute instance-attribute

SYSTEM = 'system'

An external system or service.

CONCEPT class-attribute instance-attribute

CONCEPT = 'concept'

An abstract idea.

METRIC class-attribute instance-attribute

METRIC = 'metric'

A computed business measure.

RelationshipType

Bases: StrEnum

Built-in relationship types between fields or concepts.

These are the default types shipped with pycharter. Custom types can be added via the ontology schema without modifying this enum. Use schema_loader.validate_relationship_type() for runtime validation.

REFERENCES class-attribute instance-attribute

REFERENCES = 'references'

Foreign key or reference relationship.

DERIVED_FROM class-attribute instance-attribute

DERIVED_FROM = 'derived_from'

Field is computed from another field.

SAME_AS class-attribute instance-attribute

SAME_AS = 'same_as'

Fields represent the same concept.

RELATED_TO class-attribute instance-attribute

RELATED_TO = 'related_to'

General relationship.

IS_A class-attribute instance-attribute

IS_A = 'is_a'

X is a kind of Y (taxonomy).

HAS class-attribute instance-attribute

HAS = 'has'

X contains or owns Y (composition).

DEPENDS_ON class-attribute instance-attribute

DEPENDS_ON = 'depends_on'

X requires Y to function.

PRECEDES class-attribute instance-attribute

PRECEDES = 'precedes'

X happens before Y (temporal ordering).

CAUSES class-attribute instance-attribute

CAUSES = 'causes'

X leads to or triggers Y.

IMPLEMENTS class-attribute instance-attribute

IMPLEMENTS = 'implements'

X realizes or is a concrete form of Y.

GOVERNS class-attribute instance-attribute

GOVERNS = 'governs'

X constrains or regulates Y.

DESCRIBES class-attribute instance-attribute

DESCRIBES = 'describes'

X is a property or attribute of Y.

EQUIVALENT_TO class-attribute instance-attribute

EQUIVALENT_TO = 'equivalent_to'

X represents the same thing as Y.

Parsing

parse_field_mapping

parse_field_mapping(ontology_data: dict[str, Any]) -> FieldMapping

Parse a field mapping dictionary into a FieldMapping.

Auto-detects whether the payload uses the new LinkML-backed format (domain_schema + class/slot bindings) or the legacy concept-centric format (fields + concept bindings).

Parameters:

Name Type Description Default
ontology_data dict[str, Any]

Field mapping data as dictionary.

required

Returns:

Type Description
FieldMapping

FieldMapping with parsed bindings.

Raises:

Type Description
OntologyError

If the data is invalid.

Source code in src/pycharter/semantic/ontology/parser.py
def parse_field_mapping(ontology_data: dict[str, Any]) -> FieldMapping:
    """Parse a field mapping dictionary into a FieldMapping.

    Auto-detects whether the payload uses the new LinkML-backed format
    (``domain_schema`` + class/slot bindings) or the legacy concept-centric
    format (``fields`` + concept bindings).

    Args:
        ontology_data: Field mapping data as dictionary.

    Returns:
        FieldMapping with parsed bindings.

    Raises:
        OntologyError: If the data is invalid.
    """
    if not isinstance(ontology_data, dict):
        raise OntologyError(
            f"Field mapping data must be a dictionary, got {type(ontology_data).__name__}"
        )

    if "domain_schema" in ontology_data:
        return _parse_new_format(ontology_data)
    return _parse_legacy_format(ontology_data)

parse_field_mapping_file

parse_field_mapping_file(file_path: str) -> FieldMapping

Load and parse a field mapping file (YAML or JSON).

Parameters:

Name Type Description Default
file_path str

Path to field mapping file.

required

Returns:

Type Description
FieldMapping

FieldMapping with parsed bindings.

Raises:

Type Description
OntologyError

If the file cannot be read or parsed.

Source code in src/pycharter/semantic/ontology/parser.py
def parse_field_mapping_file(file_path: str) -> FieldMapping:
    """Load and parse a field mapping file (YAML or JSON).

    Args:
        file_path: Path to field mapping file.

    Returns:
        FieldMapping with parsed bindings.

    Raises:
        OntologyError: If the file cannot be read or parsed.
    """
    path = Path(file_path)

    if not path.exists():
        raise OntologyError(f"Ontology file not found: {file_path}", path=file_path)

    suffix = path.suffix.lower()

    if suffix in (".jsonld", ".json"):
        return _parse_jsonld_file(path, file_path)

    if suffix not in (".yaml", ".yml"):
        raise OntologyError(
            f"Unsupported file format: {suffix}. "
            "Supported: .yaml, .yml, .jsonld, .json",
            path=file_path,
        )

    try:
        with open(path, encoding="utf-8") as fh:
            data = yaml.safe_load(fh)
    except yaml.YAMLError as exc:
        raise OntologyError(f"Invalid YAML: {exc}", path=file_path) from exc

    if not isinstance(data, dict):
        raise OntologyError(
            f"Ontology file must contain a dictionary, got {type(data).__name__}",
            path=file_path,
        )

    return parse_field_mapping(data)

Validation

validate_field_mapping

validate_field_mapping(artifact: FieldMapping) -> list[dict[str, str]]

Validate a FieldMapping instance.

Performs the same checks as validate_field_mapping_payload but on a parsed artifact.

Parameters:

Name Type Description Default
artifact FieldMapping

Parsed FieldMapping.

required

Returns:

Type Description
list[dict[str, str]]

List of error dicts with 'path' and 'message' keys.

list[dict[str, str]]

Empty list means valid.

Source code in src/pycharter/semantic/ontology/validator.py
def validate_field_mapping(artifact: FieldMapping) -> list[dict[str, str]]:
    """Validate a FieldMapping instance.

    Performs the same checks as validate_field_mapping_payload but on a
    parsed artifact.

    Args:
        artifact: Parsed FieldMapping.

    Returns:
        List of error dicts with 'path' and 'message' keys.
        Empty list means valid.
    """
    return validate_field_mapping_payload(artifact.to_dict())

validate_field_mapping_payload

validate_field_mapping_payload(ontology_data: dict[str, Any]) -> list[dict[str, str]]

Validate raw field mapping data before parsing.

Checks structural requirements: - Must have 'version' field - Must have 'field_bindings' dict - Each field must have 'concept' - Relationship types must be valid - No circular broader references

Parameters:

Name Type Description Default
ontology_data dict[str, Any]

Raw field mapping dictionary.

required

Returns:

Type Description
list[dict[str, str]]

List of error dicts with 'path' and 'message' keys.

list[dict[str, str]]

Empty list means valid.

Source code in src/pycharter/semantic/ontology/validator.py
def validate_field_mapping_payload(
    ontology_data: dict[str, Any],
) -> list[dict[str, str]]:
    """
    Validate raw field mapping data before parsing.

    Checks structural requirements:
    - Must have 'version' field
    - Must have 'field_bindings' dict
    - Each field must have 'concept'
    - Relationship types must be valid
    - No circular broader references

    Args:
        ontology_data: Raw field mapping dictionary.

    Returns:
        List of error dicts with 'path' and 'message' keys.
        Empty list means valid.
    """
    errors: list[dict[str, str]] = []

    if not isinstance(ontology_data, dict):
        errors.append({"path": "", "message": "Ontology must be a dictionary"})
        return errors

    # Check version
    if "version" not in ontology_data:
        errors.append(
            {"path": "version", "message": "Missing required field 'version'"}
        )

    normalized, normalization_warnings = normalize_field_mapping(ontology_data)
    for warning in normalization_warnings:
        errors.append({"path": "ontology", "message": warning})

    # Check field_bindings (canonical)
    fields = normalized.get("field_bindings")
    if fields is None:
        errors.append(
            {
                "path": "field_bindings",
                "message": "Missing required field 'field_bindings'",
            }
        )
        return errors

    if not isinstance(fields, dict):
        errors.append(
            {
                "path": "field_bindings",
                "message": "'field_bindings' must be a dictionary",
            }
        )
        return errors

    valid_rel_types = set(load_ontology_schema().relationship_types.keys())

    for field_name, field_bindings in fields.items():
        prefix = f"field_bindings.{field_name}"
        if not str(field_name).startswith("$"):
            errors.append(
                {
                    "path": prefix,
                    "message": "Field binding key should be a JSONPath starting with '$'",
                }
            )

        if isinstance(field_bindings, dict):
            field_bindings = [field_bindings]

        if not isinstance(field_bindings, list):
            errors.append(
                {
                    "path": prefix,
                    "message": f"Field '{field_name}' must be a list of bindings",
                }
            )
            continue

        for binding_idx, field_data in enumerate(field_bindings):
            item_prefix = f"{prefix}[{binding_idx}]"
            if not isinstance(field_data, dict):
                errors.append(
                    {
                        "path": item_prefix,
                        "message": "Binding must be a dictionary",
                    }
                )
                continue

            # concept is required (accept legacy "concept_id" as fallback)
            if not field_data.get("concept") and not field_data.get("concept_id"):
                errors.append(
                    {
                        "path": f"{item_prefix}.concept",
                        "message": "Missing required field 'concept'",
                    }
                )

            # tags must be a list
            tags = field_data.get("tags")
            if tags is not None and not isinstance(tags, list):
                errors.append(
                    {"path": f"{item_prefix}.tags", "message": "'tags' must be a list"}
                )

            # lineage must be a dict
            lineage = field_data.get("lineage")
            if lineage is not None and not isinstance(lineage, dict):
                errors.append(
                    {
                        "path": f"{item_prefix}.lineage",
                        "message": "'lineage' must be a dictionary",
                    }
                )

            # relationships must be a list of dicts with valid types
            relationships = field_data.get("relationships")
            if relationships is not None:
                if not isinstance(relationships, list):
                    errors.append(
                        {
                            "path": f"{item_prefix}.relationships",
                            "message": "'relationships' must be a list",
                        }
                    )
                else:
                    for i, rel in enumerate(relationships):
                        rel_prefix = f"{item_prefix}.relationships[{i}]"
                        if not isinstance(rel, dict):
                            errors.append(
                                {
                                    "path": rel_prefix,
                                    "message": "Relationship must be a dictionary",
                                }
                            )
                            continue
                        if (
                            not rel.get("target_field")
                            and not rel.get("target_concept_id")
                            and not rel.get("target")
                        ):
                            errors.append(
                                {
                                    "path": rel_prefix,
                                    "message": "Missing target_field, target_concept_id, or target",
                                }
                            )
                        rel_type = rel.get("type")
                        if rel_type is not None and rel_type not in valid_rel_types:
                            errors.append(
                                {
                                    "path": f"{rel_prefix}.type",
                                    "message": f"Invalid relationship type '{rel_type}'. "
                                    f"Valid: {sorted(valid_rel_types)}",
                                }
                            )

    # Check for circular broader references
    if isinstance(fields, dict):
        errors.extend(_check_circular_broader(fields))

    return errors

Taxonomy

classify_concept

classify_concept(concept_name: str) -> ConceptTier

Classify a concept into a governance tier.

Core concepts get their registered tier. Unknown concepts are classified as free-form (Tier 3).

Parameters:

Name Type Description Default
concept_name str

Name of the concept

required

Returns:

Type Description
ConceptTier

The governance tier for this concept

Source code in src/pycharter/semantic/ontology/taxonomy.py
def classify_concept(concept_name: str) -> ConceptTier:
    """
    Classify a concept into a governance tier.

    Core concepts get their registered tier. Unknown concepts
    are classified as free-form (Tier 3).

    Args:
        concept_name: Name of the concept

    Returns:
        The governance tier for this concept
    """
    if concept_name in CORE_CONCEPTS:
        return CORE_CONCEPTS[concept_name]["tier"]
    return ConceptTier.FREE

register_core_concept

register_core_concept(name: str, tier: ConceptTier, broader: str | None = None, description: str = '') -> None

Register a new core concept in the taxonomy.

Parameters:

Name Type Description Default
name str

Concept name

required
tier ConceptTier

Governance tier

required
broader str | None

Parent concept name (optional)

None
description str

Human-readable description

''
Source code in src/pycharter/semantic/ontology/taxonomy.py
def register_core_concept(
    name: str,
    tier: ConceptTier,
    broader: str | None = None,
    description: str = "",
) -> None:
    """
    Register a new core concept in the taxonomy.

    Args:
        name: Concept name
        tier: Governance tier
        broader: Parent concept name (optional)
        description: Human-readable description
    """
    CORE_CONCEPTS[name] = {
        "tier": tier,
        "broader": broader,
        "description": description,
    }

See also