Building a custom OPTIMADE entry type end to end

The OPTIMADE definitions guide walks through the pieces of the definition model one at a time. This example does the opposite: it plays out the whole job a database faces when it wants to serve properties of its own, start to finish, in one runnable script, and prints the actual JSON that comes out at the end.

The scenario: a fictitious database, Exampledb, serves the standard OPTIMADE calculations entry type but wants to attach three properties of its own to it — a total energy, a lattice-vector array, and a classification string. Serving custom properties correctly takes four steps.

1. Claim a prefix. OPTIMADE reserves unprefixed property names for the standard; a database-specific property must be named _<db>_<property>. The prefix is what routes the property’s canonical $id to a URL the database controls, so register_definition_prefix pairs the prefix with a base URL. The prefix itself must be a lower-case alphanumeric token wrapped in single underscores — _exmpl_ is fine, exmpl, _Exmpl_ and _exmpl are not. _httk_ and _omdb_ come pre-registered, both under httk’s ad-hoc namespace at schemas.httk.org/ad-hoc/, which is separate from the published definitions at schemas.httk.org/defs/: a synthesized $id names a property that is not published anywhere, and should not pretend otherwise.

2. Generate the property definitions. PropertyDefinition.from_simple builds a full, self-describing definition from a compact description: a name, a human-readable description, and a fulltype. The fulltype grammar covers string, integer, float, boolean, timestamp, dict, and nested lists spelled out in words — list of list of float for a 3x3 array. Array properties additionally take dimensions (named axes and their sizes) and a unit; both surface in the generated document, and dimensions also causes a list_axes metadata definition to be generated automatically.

The result is implementation-neutral: it describes the property, not how any one deployment serves it. Per-deployment flags — is this property sortable? is it in the response by default? — are layered on separately with with_implementation, which returns a new definition and leaves the original untouched.

3. Extend the entry type. EntryTypeDefinition.extended merges the custom properties into a copy of the standard definition. It refuses a name that collides with an existing property, and refuses an unprefixed custom name unless you explicitly pass allow_unprefixed=True — which is why step 1 has to happen first. Both refusals are demonstrated below.

4. Emit it. as_optimade() renders a definition as the JSON document OPTIMADE actually specifies: $schema, $id, title, description, x-optimade-type, the JSON type (with null included exactly when the property is nullable), items for lists, x-optimade-unit, x-optimade-dimensions, and the x-optimade-definition format stamp. The stamp reads "1.2" even for a v1.3 entry type such as calculations: the definition format is versioned separately from the specification, and httk-core’s generator only emits features that already exist in format 1.2.


import json
from typing import Any

from httk.core import (
    EntryTypeDefinition,
    PropertyDefinition,
    known_definition_prefixes,
    register_definition_prefix,
    standard_entry_type,
)

PREFIX = "_exmpl_"
BASE_URL = "https://schemas.example.org/ad-hoc/defs/properties"


def claim_prefix() -> None:
    """Step 1: register the database-specific prefix and its base URL."""
    print("== Step 1: claim a definition prefix ==")
    print("pre-registered prefixes:", known_definition_prefixes())
    register_definition_prefix(PREFIX, BASE_URL)
    print("after registering:      ", known_definition_prefixes())

    for bad in ("exmpl", "_Exmpl_", "_exmpl", "exmpl_"):
        try:
            register_definition_prefix(bad, BASE_URL)
        except ValueError:
            print(f"  rejected malformed prefix {bad!r}")
    print()


def build_properties() -> dict[str, PropertyDefinition]:
    """Step 2: generate the custom property definitions."""
    print("== Step 2: generate the custom property definitions ==")
    properties = {
        "_exmpl_total_energy": PropertyDefinition.from_simple(
            "_exmpl_total_energy",
            description="Total energy of the calculation.",
            fulltype="float",
            unit="eV",
        ),
        "_exmpl_lattice_vectors": PropertyDefinition.from_simple(
            "_exmpl_lattice_vectors",
            description="The three lattice vectors of the calculated cell.",
            fulltype="list of list of float",
            unit="angstrom",
            dimensions={"names": ["dim_lattice", "dim_spatial"], "sizes": [3, 3]},
        ),
        "_exmpl_wave_class": PropertyDefinition.from_simple(
            "_exmpl_wave_class",
            description="Altermagnetic wave class of the calculated material.",
            fulltype="string",
        ),
    }
    for name, definition in properties.items():
        print(f"  {name}: {definition.optimade_type}, unit={definition.unit}")
        print(f"      $id: {definition.definition_id}")

    # Per-deployment flags are an overlay; the neutral definition is unchanged.
    served = properties["_exmpl_total_energy"].with_implementation(sortable=True, response_default=False)
    print("  overlay on _exmpl_total_energy:", served.as_optimade()["x-optimade-implementation"])
    print(
        "  neutral original still has no overlay:",
        "x-optimade-implementation" not in properties["_exmpl_total_energy"].as_optimade(),
    )
    print()
    return properties


def extend_entry_type(properties: dict[str, PropertyDefinition]) -> EntryTypeDefinition:
    """Step 3: merge the custom properties into a copy of the standard entry type."""
    print("== Step 3: extend the standard 'calculations' entry type ==")
    calculations = standard_entry_type("calculations")
    print(f"standard 'calculations' describes {len(calculations.properties)} properties:")
    print("  ", ", ".join(calculations.properties))

    extended = calculations.extended(properties)
    print(f"extended entry type describes {len(extended.properties)} properties:")
    print("  ", ", ".join(extended.properties))
    print("the standard definition is untouched:", "_exmpl_total_energy" not in calculations.properties)

    # A name that collides with an existing property is refused ...
    try:
        calculations.extended({"id": PropertyDefinition.from_simple("id", description="dup")})
    except ValueError as exc:
        print("  collision refused:", exc)

    # ... and so is an unprefixed custom name, unless explicitly allowed.
    cogwheels = PropertyDefinition.from_simple("cogwheels", description="Cogwheels.", fulltype="integer")
    try:
        calculations.extended({"cogwheels": cogwheels})
    except ValueError as exc:
        print("  unprefixed name refused:", exc)
    print(
        "  allow_unprefixed=True lets it through:",
        "cogwheels" in calculations.extended({"cogwheels": cogwheels}, allow_unprefixed=True).properties,
    )
    print()
    return extended


def emit(extended: EntryTypeDefinition) -> None:
    """Step 4: render the definitions as OPTIMADE JSON documents."""
    print("== Step 4: as_optimade() ==")
    lattice_vectors: dict[str, Any] = extended.properties["_exmpl_lattice_vectors"].as_optimade()
    print("_exmpl_lattice_vectors as an OPTIMADE property definition:")
    print(json.dumps(lattice_vectors, indent=2, sort_keys=True))

    print()
    print("Whole-entry-type document keys:", sorted(extended.as_optimade()))


def main() -> None:
    claim_prefix()
    properties = build_properties()
    extended = extend_entry_type(properties)
    emit(extended)


if __name__ == "__main__":
    main()